개발하다보면 꼭 1번씩은 마주친 CORS에러👿
빨간색이라 마주칠때마다 너무너무 싫었는데
CORS가 왜 필요한건지 SOP에 대해 알게되었고
이젠 CORS에러를 보아도 브라우저가 열일하고 있구나 하고 너그러운 마음을 품을 수 있게 되었다(?)🤗
:한 출처에서 다른 출처의 자원에 접근할 수 있도록 브라우저에게 알려주는 체제이다.
다른 출저 자원에 접근하는 경우는 fetch로 직접 요청을 보내는 경우나, 웹 폰트 import 시, img src, a href와 같이 해당 자원을 읽어오는 경우에도 요청이 필요하다.
CORS를 제대로 알려면 SOP를 먼저 알아야 한다.
여기서 말하는 Origin이 요청을 주고받는 Url을 의미하는데
이는 정확히 따지자면 document.loation.origin
이다.
지금 현재 벨로그에서 콘솔창을 열고 위와 같이 입력해보면 origin이 "https://velog.io" 임을 알 수 있다.
동일한 출처는 요청을 주고 받는 두 URL의 프로토콜(scheme),호스트, 도메인, 포트가 모두 같은 것을 의미한다.
https://velog.io/write
=== https://velog.io/list
=>경로만 다른 경우
https://velog.io/write
=== http://velogs.io/write
=>프로토콜도 다르고, 도메인도 다르다.
예시) 시나리오를 들면, 해커가 요상한 사이트를 만들어 유저에게 해커의 사이트로 접속하도록하고, 사이트의 스크립트 파일에
<img src=`중요한 정보가 담긴 사이트`/>
를 담아 보내면 유저의 document에서 이미지를 로드하듯이 중요한 정보가 담긴 외부 사이트 document를 로드해오게 되고 이때 사용자가 해당 사이트에 접속되었던 상태였다면 쿠키까지 붙혀져 서버는 사용자의 접속으로 간주하고 응답을 보내주게 된다. 그럼 외부document의 응답을 인식하고 해킹사이트로 전송시키는 요청을 보내면서 보안이 털리게 된다.
(아래 설명의 cross-origin reads가 가능하다면의 상황, 만약 SOP가 적용된다면 외부 Document의 응답은 받아왔지만 해당값을 인식할 수 없게 된다.=> CORS에러)
SOP가 적용되면 응답 데이터를 인지하지 못하고(Read불가능) 다른 사이트로의 전달이 막혀 해커가 데이터에 접근할수 없게 되는 것이다. 다르게 말하면 중요한 정보를 요청받아오는 일까지는 가능하다는 것이다.
즉 SOP자체도 보안이 완벽하다곤 할 수 없다. 교차출처에 대하여 write, embedding이 가능하기 때문이다.
cross-origin writes 가능 : 다른 서버에 자원을 요청하는 행위 자체는 가능함 (GET요청으로 데이터를 가져오게 하거나, POST요청으로 데이터를 수정하거나)
=> *CSRF, 사이트간 요청위조 공격 (서버가 사용자의 사이트를 신뢰하는 상황)
예시) 해커가 지정한 비밀번호로 외부사이트의 비밀번호를 변경하도록 하는 요청을 보내게 한다. 비밀번호가 변경되면 사용자의 아이디로 로그인하게 된다.(서비스 관리자 계정으로 로그인해서 전체 서비스의 에러를 발생시키거나 서비스 사용자 정보를 유출할수있게 된다.)
=>해결방법 same-site!
cross-origin embedding 가능 : 상대 origin 의 script에 <script>
나 css, <img><iframe>
등을 심어버릴 수 있다. 그럼 사용자 브라우저는 script를 읽으며 해당 리소스를 받아오기 위해 리소스 주소로 요청을 보내게 된다.
=> *XSS 공격(사용자가 사이트를 신뢰하는 상황)
예시)해당 origin에서 세션ID가 담긴 쿠키를 탈취하여 해커에게 보내거나 다른 곳으로 리다이렉션 등 비정상 동작을 일으킴
=> (Script의 유효성을 검증하는 단계가 필요하다. )
위의 공격상황을 고려한 보안이 더 요구되고 있고
때문에 SOP가 최소한의 맞는 방법이지만 요즘은 필요한 정보의 범위가 넓기에 CORS가 일부 허용되어야 하는 것이다.
예비요청과 본 요청으로 나누어서 서버에 요청을 전송하는데
예비요청을 preflight라고 하고 이때 HTTP메소드는 OPTIONS, 본 요청 전에 서버에서 요청 메서드와 헤더에 대해 인식하는지를 체크하는 것이다. 이때 preFlight의 응답메시지의 헤더에서
Access-Contrl-Allow-Headers
Access-Contrl-Allow-Methods
Access-Contrl-Allow-Origin
이 값들이 본 요청에서 보낼 내용과 일치하는지를 확인한다.
우선 이 정보를 받아오면 statusCode 200번을 볼 수 있다.
특히 CORS관련해서는 Access-Contrl-Allow-Origin에서 Origin이 요청을 보낸 Origin과 동일한지를 확인한다. 동일하다면 교차출처요청이 가능하니 다음으로 본 요청을 보내게 된다.
만일, 두 Origin이 다르다면 그 다음 본 요청이 보내지지 않고 CORS에러가 나게 되는 것이다.
이 preflight는 매번 보낼수도 있지만, 캐싱 조건을 커스텀할수도 있다. (ex_해당 기간 동안은 preflight를 캐쉬하여 사용하겠음)
HTTP Cookie와 HTTP Authentication 정보를 인식할 수 있게 해주는 요청,
axios.post(API.LOGIN, contents, { withCredentials: true })
리액트-node.js 환경에서의 해결방법을 살펴보면...
접속을 허용할 origin을 "Access-Control-Allow-Origin"에 추가한다.
(모든 origin을 허가하지 않도록 주의한다.)
Express에서는 아래와 같은 방법으로 CORS 허가를 설정할 수 있다.
const prod = process.env.NODE_ENV === 'production';
if (prod) {
app.enable('trust proxy');
app.use(morgan('combined'));
app.use(helmet({ contentSecurityPolicy: false }));
app.use(hpp());
} else {
app.use(morgan('dev'));
app.use( > //*cors허가 설정
cors({
origin: true,
credentials: true,
})
);
}
origin: true -> 프론트 도메인 주소가 자동으로 Access-Control-Allow-Origin으로 설정 (*: 전부허용)
credentials: true 요청에 쿠키를 포함하고 싶을 때, include:모든 요청에 인증정보를 포함하겠다., omit:인증정보 담지않음
서버와 클라이언트가 포트넘버는 다르고 로컬주소가 동일한 경우,
(ex_ 서버:http://localhost:3095, 클라이언트: http://localhost:3090) Proxy를 통해 해결을 한다. 클라이언트에서 서버로 요청을 보낼 때 port넘버가 달라 찾을 데이터 값이 없다면 proxy로 설정해둔 주소로 대신 요청을 보내게 되는 것이다. 클라이언트와 서버의 중계역할을 담당한다. (proxy가 어원적 의미로 '대리'라는 뜻을 갖고있다.)
(로컬개발단계에서의 해결방법이고, 로컬주소도 동일할 때 가능한 방법이라 결국은 백엔드에서의 origin설정이 정답이 되긴한다)
devServer proxy설정
devServer: {
historyApiFallback: true,
port: ${클라이언트 포트넘버},
publicPath: '/dist/',
proxy: {
'/api/': {
target: `http://localhost:${서버포트넘버}`,
changeOrigin: true,
},
},
},
/api/로 시작하는 요청을 서버포트주소에서 보내는 것 처럼 취급하겠다는 설정이다.
클라이언트에서는 기존의 localhost:3000/api/users/
를 localhost부분을 삭제하고 /api/users
로 변경해준다.
axios.post(`/api/users`)
이 때 특징으로는 동일한 주소로 이미 판단하여 preflight를 보내지 않는다는 점이다.
package.json에서 proxy설정
},
...
"proxy": "http://localhost:4000",
...
{