교차 출처 리소스 공유(Cross-Origin Resource Sharing; CORS)는 클라이언트에서 다른 출처의 리소스에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 정책을 의미함.
1. 용어
A. 동일 출처(Same-Origin)
출처(Origin)는 웹페이지에 접근할 때 사용되는 URL을 의미하며,Scheme,Hosts,Port로 구성됨. 세 요소가 모두 같은 출처가 동일 출처.
예를 들어,
http://localhost:3000/
http://localhost:1000/
두 출처는 Scheme, Hosts는 동일하지만 Port가 다름. 따라서 동일 출처가 아님.
http://www.example.com
https://www.example.com
두 출처는 Hosts는 동일하지만Scheme,Port(http는 80번 포트, https는 443번 포트)가 다름. 따라서 동일 출처가 아님.
B. 프리플라이트(Preflight) 요청
교차 출처 요청을 하기 전에 해당 리소스에 대한 접근 권한이 있는지 확인하는 과정으로, "Access-Control-Request-Method", "Access-Control-Request-Headers", "Access-Control-Allow-Origin" 총 3가지의 HTTP Request Headers를 사용하는 OPTIONS 요청.
A. 실제 요청을 보내기 전에 권한 유무 먼저 확인하기 때문에 리소스 면에서 효율적.
B. CORS 이전에 만들어진 서버는 동일 출처 정책(SOP)을 전제로 만들어졌기 때문에 다른 출처에서 보낸 요청도 처리함. 만약 DELETE, POST 등의 교차 출처 요청이 악의적인 목적으로 이뤄졌다면 서버 리소스에 어떤 짓을 할지 모름.
프리플라이트 요청이 불필요한 트래픽을 유발한다고 생각할 수 있지만 위와 같은 이유 때문에 중요함.
2. 동일 출처 정책(Same-Origin Policy)
웹페이지에서 다운로드 한 문서나 스크립트가다른 출처에서 가져온 리소스일 경우, 해당 리소스에 접근하는 것을 제한하는 보안 정책. 예외적으로 <img>, <script>, <link>, <iframe>과 같은 특정 HTML 태그만 다른 출처에서 온 요청을 허용하기 때문에 CDN을 이용할 수 있음. SOP 덕분에 외부의 공격으로부터 안전할 수 있지만 다른 출처에서 리소스를 가져오고 싶을 때는 오히려 걸림돌이 될 수 있음. 이때 CORS 정책을 준수하는 요청을 보내 리소스 접근을 가능하게 할 수 있음.
출처를 비교하는 로직은 브라우저에 구현되어 있는 스펙이기 때문에 만약 우리가 CORS 정책을 위반하는 리소스 요청을 하더라도 해당 서버가 같은 출처에서 보낸 요청만 받겠다는 로직을 가지고 있는 경우가 아니라면 서버는 정상적으로 응답을 보내 줌. 이후브라우저가 이 응답을 확인해서CORS 정책 위반이라고 판단되면 그 응답을 사용하지 않고 그냥 버림.
3. CORS가 작동하는 방식
A. 단순 요청(Simple Requests)
① GET, HEAD, POST 중 하나의 메서드만 사용할 것
② "Accept", "Accept-Language", "Content-Language", "Content-Type" 이외의 헤더를 사용하지 않을 것
③ "Content-Type" 헤더의 값으로 "application/x-www-form-urlencoded", "multipart/form-data", "text/plain"만 사용할 것
위 세 가지의 조건을 만족하는 클라이언트의 요청에 대해서 서버는 응답으로 "Access-Control-Allow-Origin" 헤더를 포함해서 전송함. 브라우저는 해당 헤더의 값을 확인하고 그 값이 클라이언트의 출처와 동일하면 서버의 리소스에 접근할 수 있게 함.
// 서버 엔드포인트 설정 1: "Access-Control-Allow-Origin" 미설정할 경우
app.post("/", (req, res) => {
res.send('OK');
}
// 서버 엔드포인트 설정 2: "Access-Control-Allow-Origin" 값이 클라이언트 주소와 다를 경우
app.post("/", (req, res) => {
res.set("Access-Control-Allow-Origin", "https://foo.example");
res.send('OK');
}
예를 들어, 클라이언트(http://localhost:3000/)에서 위와 같이 POST 요청을 보낼 때 서버가 응답으로 보낸 "Access-Control-Allow-Origin" 헤더값(https://foo.example)은 클라이언트의 주소와 다르기 때문에 아래와 같은 경고를 출력함. 한편, 서버에서 해당 헤더값을"http://localhost:3000/"로 지정해줄 경우, 루트 경로인http://localhost:3000/에서만 접속이 허용되고 http://localhost:3000/other_path 에서는 접속이 차단됨. 해당 도메인 전체에서 접속을 허용하고자 경우,"http://localhost:3000" 처럼 /를 제외해야 함에 주의!
1. 서버에서 "Access-Control-Allow-Origin" 미설정 시2. 서버가 응답으로 보낸 "Access-Control-Allow-Origin"이 클라이언트 주소와 다른 경우
서버에 요청을 보낼 때 단순 요청의 세 가지 조건을 만족하는 요청은 흔하지 않음. 단순 요청에 해당하지 않는 경우 브라우저는 프리플라이트 요청을 보내 메인 요청을 보내기 전에 권한을 확인하는 과정을 거침. 예를 들어, 사이트 간 통신에 주로 쓰이는 JSON 데이터를 서버에 전송하려 할 경우"Content-Type": "application/json"으로 지정해줘야 함. 이 상태에서 서버에 요청을 보내면 서버의 응답 헤더에 "Access-Control-Allow-Origin": "http://localhost:3000"를 설정했는데도 아래와 같은 CORS 에러가 발생함.
개발자 도구 네트워크 탭을 보면 위 요청은 단순 요청의 ③번 조건에 부합하지 않기 때문에 클라이언트에서 서버에 POST 요청을 보내기 전에 "이렇게 POST 요청 보내려고 하는데 가능한가요?"라고 묻는 프리플라이트 요청을 먼저 보내는 것을 확인할 수 있음.
프리플라이트 요청은 성공했지만...
위 그림에서 알 수 있듯이 프리플라이트 요청은 성공했지만 아까처럼"Access-Control-Allow-Origin"이 설정되지 않았다는 경고 메시지가 출력됨. 그 이유는 메인 요청은 POST이지만프리플라이트 요청은 OPTIONS이기 때문. 현재 해당 서버에 OPTIONS 엔드포인트가 없는 상태이기 때문에 OPTIONS 응답 헤더에 "Access-Control-Allow-Origin" 필드가 없음.
반대로, 프리플라이트 요청(OPTIONS)의 응답 헤더에 "Access-Control-Allow-Origin", "Access-Control-Allow-Headers"를 모두 넣어줬더라도 메인 요청인 POST 응답 헤더에 "Access-Control-Allow-Origin"이 설정되어 있지 않다면 역시 오류가 발생함.
3. POST 응답에 "Access-Control-Allow-Origin" 헤더를 주지 않았을 경우
따라서, OPTIONS, POST 메서드 모두 "Access-Control-Allow-Origin"에서 클라이언트의 주소를 허용해야 하므로 app.get(), app.options()에 따로 입력하는 것보다 위 코드처럼 상단에 별도의 미들웨어를 만들어서 항상 처리될 수 있도록 하는 편이 좋음.
// cors 모듈을 사용하는 방법
const cors = require('cors');
app.use(cors({
origin: "http://localhost:3000",
methods: ['POST', 'OPTIONS'],
}))
또는, cors 모듈을 사용해서 최상단 미들웨어로 지정해주는 것이 좋음. "Access-Control-Allow-Headers" 헤더를 설정해주는allowedHeaders 속성의 기본값은 요청 객체의 "Access-Control-Request-Headers"(이 경우에는 "Content-Type") 값으로 지정되므로 별도로 설정해줄 필요는 없음.
C. 인증 정보를 포함한 요청(Credentialed Requests)
CORS는 보통 단순 요청, 프리플라이트 요청 두가지 방식으로 작동하지만, 다른 출처 간 통신에서 쿠키를 보내지 않음. 이럴 경우, 사이트 간 요청 위조 공격(Cross-Site Request Forgery; CSRF)에 취약하기 때문에 요청에 쿠키를 포함해 보안을 강화하고 싶을 때 이 방법을 사용할 수 있음. 인증 정보를 포함한 요청을 하기 위해선 아래 두 가지 조건을 만족해야 함.
① 응답 헤더에"Access-Control-Allow-Origin": "*"는 허용되지 않음(명확한 주소를 특정해야 함)
② 응답 헤더에 "Access-Control-Allow-Credentials": true가 있어야 함
Express.js에서 cors 모듈을 사용할 때 프리플라이트 요청인 OPTIONS 메서드는 methods 속성에 넣어주지 않아도 네트워크 요청은 문제 없이 이뤄짐. OPTIONS는 서버의 리소스에 접근하기 전에 권한을 확인하기 위한 메서드인데 OPTIONS 요청을 보내기 위해 OPTIONS 권한을 확인하는 요청을 거치고, 그 전에 그 요청을 할 수 있는 권한이 있는지 ... 재귀적인 검증이 이뤄지므로 현실적으로도 OPTIONS 메서드를 허용해야 한다는 개념은 말도 안 됨. 따라서, 허용할 메서드에 OPTIONS는 포함시킬 필요가 없음.
위 사진처럼 Authorization 헤더는 CORS 요청에서 클라이언트가 바로 접근할 수 있는 응답 헤더가 아니기 때문에 서버에서 Authorization 헤더를 보내준다면 Authorization 헤더를 보내줄 때 Access-Control-Expose-Headers에 Authorization을 포함해서 보내줘야 함.
6. 기타 CORS 오류
A. Redirect is not allowed for a preflight request
서버 요청을 보냈는데 위와 같은 CORS 오류가 뜨는 경우가 있음. 오류 메시지 그대로 프리플라이트 요청에서는 리다이렉트가 허용되지 않지만 리다이렉트 요청이 발생했기 때문.
요청 URL과 응답 Location 헤더가 다름
위 응답 Location 헤더를 보면 요청 URL과 스킴(Scheme)이 https, http로 다름을 알 수 있음. 유효한 서버 주소가 https이기 때문에프리플라이트 요청에서 http가 https로 리다이렉트 되었기 때문에발생한 문제. 요청 주소를 제대로 수정하면 문제는 해결됨.
7. 요약
단순 요청 "Access-Control-Allow-Origin"
프리플라이트 요청 OPTIONS를 포함한 POST 등의 요청에 대해 ① "Access-Control-Allow-Origin" ② "Access-Control-Allow-Headers"
인증 정보를 포함한 요청 OPTIONS를 포함한 POST 등의 요청에 대해 ① "Access-Control-Allow-Origin": "*"는 허용되지 않음 ② "Access-Control-Allow-Credentials": true
오류 메시지로 살펴보는 CORS 에러
교차 출처 리소스 공유(Cross-Origin Resource Sharing; CORS)는 클라이언트에서 다른 출처의 리소스에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 정책을 의미함.
1. 용어
A. 동일 출처(Same-Origin)
출처(Origin)는 웹페이지에 접근할 때 사용되는 URL을 의미하며, Scheme, Hosts, Port로 구성됨. 세 요소가 모두 같은 출처가 동일 출처.
예를 들어,
두 출처는 Scheme, Hosts는 동일하지만 Port가 다름. 따라서 동일 출처가 아님.
두 출처는 Hosts는 동일하지만 Scheme, Port(http는 80번 포트, https는 443번 포트)가 다름. 따라서 동일 출처가 아님.
B. 프리플라이트(Preflight) 요청
교차 출처 요청을 하기 전에 해당 리소스에 대한 접근 권한이 있는지 확인하는 과정으로, "Access-Control-Request-Method", "Access-Control-Request-Headers", "Access-Control-Allow-Origin" 총 3가지의 HTTP Request Headers를 사용하는 OPTIONS 요청.
A. 실제 요청을 보내기 전에 권한 유무 먼저 확인하기 때문에 리소스 면에서 효율적.
B. CORS 이전에 만들어진 서버는 동일 출처 정책(SOP)을 전제로 만들어졌기 때문에 다른 출처에서 보낸 요청도 처리함. 만약 DELETE, POST 등의 교차 출처 요청이 악의적인 목적으로 이뤄졌다면 서버 리소스에 어떤 짓을 할지 모름.
프리플라이트 요청이 불필요한 트래픽을 유발한다고 생각할 수 있지만 위와 같은 이유 때문에 중요함.
2. 동일 출처 정책(Same-Origin Policy)
웹페이지에서 다운로드 한 문서나 스크립트가 다른 출처에서 가져온 리소스일 경우, 해당 리소스에 접근하는 것을 제한하는 보안 정책. 예외적으로 <img>, <script>, <link>, <iframe>과 같은 특정 HTML 태그만 다른 출처에서 온 요청을 허용하기 때문에 CDN을 이용할 수 있음. SOP 덕분에 외부의 공격으로부터 안전할 수 있지만 다른 출처에서 리소스를 가져오고 싶을 때는 오히려 걸림돌이 될 수 있음. 이때 CORS 정책을 준수하는 요청을 보내 리소스 접근을 가능하게 할 수 있음.
출처를 비교하는 로직은 브라우저에 구현되어 있는 스펙이기 때문에 만약 우리가 CORS 정책을 위반하는 리소스 요청을 하더라도 해당 서버가 같은 출처에서 보낸 요청만 받겠다는 로직을 가지고 있는 경우가 아니라면 서버는 정상적으로 응답을 보내 줌. 이후 브라우저가 이 응답을 확인해서 CORS 정책 위반이라고 판단되면 그 응답을 사용하지 않고 그냥 버림.
3. CORS가 작동하는 방식
A. 단순 요청(Simple Requests)
① GET, HEAD, POST 중 하나의 메서드만 사용할 것
② "Accept", "Accept-Language", "Content-Language", "Content-Type" 이외의 헤더를 사용하지 않을 것
③ "Content-Type" 헤더의 값으로 "application/x-www-form-urlencoded", "multipart/form-data", "text/plain"만 사용할 것
위 세 가지의 조건을 만족하는 클라이언트의 요청에 대해서 서버는 응답으로 "Access-Control-Allow-Origin" 헤더를 포함해서 전송함. 브라우저는 해당 헤더의 값을 확인하고 그 값이 클라이언트의 출처와 동일하면 서버의 리소스에 접근할 수 있게 함.
예를 들어, 클라이언트(http://localhost:3000/)에서 위와 같이 POST 요청을 보낼 때 서버가 응답으로 보낸 "Access-Control-Allow-Origin" 헤더값(https://foo.example)은 클라이언트의 주소와 다르기 때문에 아래와 같은 경고를 출력함. 한편, 서버에서 해당 헤더값을 "http://localhost:3000/"로 지정해줄 경우, 루트 경로인 http://localhost:3000/에서만 접속이 허용되고 http://localhost:3000/other_path 에서는 접속이 차단됨. 해당 도메인 전체에서 접속을 허용하고자 경우, "http://localhost:3000" 처럼 /를 제외해야 함에 주의!
B. 프리플라이트 요청(Preflight Requests)
서버에 요청을 보낼 때 단순 요청의 세 가지 조건을 만족하는 요청은 흔하지 않음. 단순 요청에 해당하지 않는 경우 브라우저는 프리플라이트 요청을 보내 메인 요청을 보내기 전에 권한을 확인하는 과정을 거침. 예를 들어, 사이트 간 통신에 주로 쓰이는 JSON 데이터를 서버에 전송하려 할 경우 "Content-Type": "application/json"으로 지정해줘야 함. 이 상태에서 서버에 요청을 보내면 서버의 응답 헤더에 "Access-Control-Allow-Origin": "http://localhost:3000"를 설정했는데도 아래와 같은 CORS 에러가 발생함.
개발자 도구 네트워크 탭을 보면 위 요청은 단순 요청의 ③번 조건에 부합하지 않기 때문에 클라이언트에서 서버에 POST 요청을 보내기 전에 "이렇게 POST 요청 보내려고 하는데 가능한가요?"라고 묻는 프리플라이트 요청을 먼저 보내는 것을 확인할 수 있음.
위 그림에서 알 수 있듯이 프리플라이트 요청은 성공했지만 아까처럼 "Access-Control-Allow-Origin"이 설정되지 않았다는 경고 메시지가 출력됨. 그 이유는 메인 요청은 POST이지만 프리플라이트 요청은 OPTIONS이기 때문. 현재 해당 서버에 OPTIONS 엔드포인트가 없는 상태이기 때문에 OPTIONS 응답 헤더에 "Access-Control-Allow-Origin" 필드가 없음.
앞서 app.post()에 "Access-Control-Allow-Origin"헤더를 설정해준 것처럼 app.options()에도 동일하게 설정해줬지만 이번엔 다른 오류가 발생함.
반대로, 프리플라이트 요청(OPTIONS)의 응답 헤더에 "Access-Control-Allow-Origin", "Access-Control-Allow-Headers"를 모두 넣어줬더라도 메인 요청인 POST 응답 헤더에 "Access-Control-Allow-Origin"이 설정되어 있지 않다면 역시 오류가 발생함.
따라서, OPTIONS, POST 메서드 모두 "Access-Control-Allow-Origin"에서 클라이언트의 주소를 허용해야 하므로 app.get(), app.options()에 따로 입력하는 것보다 위 코드처럼 상단에 별도의 미들웨어를 만들어서 항상 처리될 수 있도록 하는 편이 좋음.
또는, cors 모듈을 사용해서 최상단 미들웨어로 지정해주는 것이 좋음. "Access-Control-Allow-Headers" 헤더를 설정해주는allowedHeaders 속성의 기본값은 요청 객체의 "Access-Control-Request-Headers"(이 경우에는 "Content-Type") 값으로 지정되므로 별도로 설정해줄 필요는 없음.
C. 인증 정보를 포함한 요청(Credentialed Requests)
CORS는 보통 단순 요청, 프리플라이트 요청 두가지 방식으로 작동하지만, 다른 출처 간 통신에서 쿠키를 보내지 않음. 이럴 경우, 사이트 간 요청 위조 공격(Cross-Site Request Forgery; CSRF)에 취약하기 때문에 요청에 쿠키를 포함해 보안을 강화하고 싶을 때 이 방법을 사용할 수 있음. 인증 정보를 포함한 요청을 하기 위해선 아래 두 가지 조건을 만족해야 함.
① 응답 헤더에 "Access-Control-Allow-Origin": "*"는 허용되지 않음(명확한 주소를 특정해야 함)
② 응답 헤더에 "Access-Control-Allow-Credentials": true가 있어야 함
클라이언트에서 요청을 보낼 때 credentials 속성을 추가해주고, 서버에도 "Access-Control-Allow-Credentials": true를 설정해줘야 함.
4. 프리플라이트 요청과 Access-Control-Request-Method
Express.js에서 cors 모듈을 사용할 때 프리플라이트 요청인 OPTIONS 메서드는 methods 속성에 넣어주지 않아도 네트워크 요청은 문제 없이 이뤄짐. OPTIONS는 서버의 리소스에 접근하기 전에 권한을 확인하기 위한 메서드인데 OPTIONS 요청을 보내기 위해 OPTIONS 권한을 확인하는 요청을 거치고, 그 전에 그 요청을 할 수 있는 권한이 있는지 ... 재귀적인 검증이 이뤄지므로 현실적으로도 OPTIONS 메서드를 허용해야 한다는 개념은 말도 안 됨. 따라서, 허용할 메서드에 OPTIONS는 포함시킬 필요가 없음.
5. 클라이언트에서 접근가능한 헤더
CORS 요청에서 클라이언트는 특정한 응답 헤더에만 접근할 수 있는데 이것을 CORS-safelisted Response Header라고 함. 만약, 클라이언트에서 그외의 응답 헤더에 접근할 필요가 있다면 서버에서 Access-Control-Expose-Headers 헤더에 해당 헤더를 설정해서 보내줘야 함.
위 사진처럼 Authorization 헤더는 CORS 요청에서 클라이언트가 바로 접근할 수 있는 응답 헤더가 아니기 때문에 서버에서 Authorization 헤더를 보내준다면 Authorization 헤더를 보내줄 때 Access-Control-Expose-Headers에 Authorization을 포함해서 보내줘야 함.
6. 기타 CORS 오류
A. Redirect is not allowed for a preflight request
서버 요청을 보냈는데 위와 같은 CORS 오류가 뜨는 경우가 있음. 오류 메시지 그대로 프리플라이트 요청에서는 리다이렉트가 허용되지 않지만 리다이렉트 요청이 발생했기 때문.
위 응답 Location 헤더를 보면 요청 URL과 스킴(Scheme)이 https, http로 다름을 알 수 있음. 유효한 서버 주소가 https이기 때문에 프리플라이트 요청에서 http가 https로 리다이렉트 되었기 때문에 발생한 문제. 요청 주소를 제대로 수정하면 문제는 해결됨.
7. 요약
[참고]
https://coding-groot.tistory.com/91
https://evan-moon.github.io/2020/05/21/about-cors/
What exactly does the Access-Control-Allow-Credentials header do?
Should Access-Control-Allow-Methods include OPTIONS
How to resolve 'preflight is invalid (redirect)' or 'redirect is not allowed for a preflight request'
Axios get access to response header fields
'네트워크' 카테고리의 다른 글