안녕하세요! 프론트엔드 개발자라면 누구나 한 번쯤은 브라우저 콘솔 창을 시뻘겋게 물들이는 그 악명 높은 에러, 바로 CORS(교차 출처 리소스 공유)를 만나게 됩니다.
실무에서 서버 개발자와 가장 많이 소통(혹은 다툼)하게 만드는 주제이기도 하죠. 이 문서를 꼼꼼히 읽고 원리를 완벽하게 이해해 두시면, 나중에 API를 연동할 때 문제가 생겨도 당황하지 않고 원인을 정확히 파악할 수 있을 거예요. 자, 원본 내용 그대로 상세한 설명과 제 실무 팁을 곁들여 번역해 드릴게요!
Baseline (널리 사용 가능)
이 기능은 잘 확립되어 있으며 수많은 기기와 브라우저 버전에서 작동합니다. 2015년 7월부터 모든 주요 브라우저에서 사용할 수 있습니다.
교차 출처 리소스 공유 (CORS)는 서버가 브라우저에게 자신의 출처(도메인, 스킴, 또는 포트)가 아닌 다른 출처(origins)에서 리소스를 로드하는 것을 허용하도록 알려주는 HTTP 헤더 기반의 메커니즘입니다. 또한 CORS는 브라우저가 교차 출처 리소스를 호스팅하는 서버에게 "이 실제 요청을 보내도 안전한지" 확인하기 위해 "사전 요청(preflight)"을 보내는 메커니즘에 의존합니다. 이 사전 요청에서 브라우저는 실제 요청에서 사용될 HTTP 메서드와 헤더들을 미리 서버에 알려줍니다.
교차 출처 요청의 예: https://domain-a.com에서 서비스되는 프론트엔드 JavaScript 코드가 fetch()를 사용하여 https://domain-b.com/data.json으로 데이터를 요청하는 경우입니다.
💡 강사의 팁: 프론트엔드 로컬 개발 환경(예:
localhost:3000)에서 백엔드 개발 서버(예:localhost:8080)로 API 요청을 보낼 때, 도메인은 같아도 '포트'가 다르기 때문에 브라우저는 이를 '다른 출처(Cross-Origin)'로 인식합니다. 그래서 로컬 개발 중에도 이 CORS 에러를 수시로 만나게 되는 것이죠!
보안상의 이유로, 브라우저는 스크립트에서 시작된 교차 출처 HTTP 요청을 엄격하게 제한합니다. 예를 들어, fetch()와 XMLHttpRequest는 동일 출처 정책(same-origin policy)을 따릅니다. 이는 이러한 API를 사용하는 웹 애플리케이션이 자신을 로드한 곳과 똑같은 출처(동일 출처)에서만 리소스를 요청할 수 있음을 의미합니다. 단, 다른 출처에서 온 응답에 올바른 CORS 헤더가 포함되어 있다면 예외적으로 허용됩니다.
CORS 메커니즘은 브라우저와 서버 간의 안전한 교차 출처 요청 및 데이터 전송을 지원합니다. 브라우저는 fetch()나 XMLHttpRequest 같은 API에서 교차 출처 HTTP 요청의 위험을 완화하기 위해 CORS를 사용합니다.
이 교차 출처 공유 표준은 다음과 같은 경우에 교차 출처 HTTP 요청을 가능하게 해줍니다:
fetch()나 XMLHttpRequest의 호출.@font-face를 통한 교차 도메인 폰트 사용). 폰트 가져오기 요구사항에 설명된 대로, 서버는 허용된 웹사이트에서만 교차 출처로 로드하여 사용할 수 있는 TrueType 폰트를 배포할 수 있습니다.drawImage()를 사용하여 캔버스(canvas)에 그려지는 이미지나 비디오 프레임.이 문서는 교차 출처 리소스 공유(CORS)에 대한 전반적인 내용을 다루며, 필요한 HTTP 헤더들에 대한 논의도 포함하고 있습니다.
교차 출처 리소스 공유(CORS) 표준은 서버가 어떤 출처(웹 브라우저)에서 해당 정보를 읽을 수 있도록 허락할지 설명하는 새로운 HTTP 헤더들을 추가함으로써 작동합니다. 게다가 서버의 데이터에 부수 효과(side-effects)를 일으킬 수 있는 HTTP 요청 메서드들(특히 GET이 아니거나 특정 MIME 타입을 가진 POST가 아닌 메서드들)에 대해서, 명세는 브라우저가 요청을 "사전 전달(preflight)"하도록 강제합니다. 브라우저는 HTTP OPTIONS 요청 메서드로 서버가 지원하는 메서드들을 먼저 물어보고, 서버로부터 "승인(approval)"을 받은 후에야 비로소 실제 요청을 보냅니다. 서버는 또한 클라이언트에게 요청과 함께 "자격 증명(credentials)"(예: 쿠키나 HTTP 인증)을 보내야 하는지도 알려줄 수 있습니다.
CORS가 실패하면 에러가 발생하지만, 보안상의 이유로 에러에 대한 구체적인 내용은 JavaScript에 제공되지 않습니다. 코드가 알 수 있는 것은 그저 "에러가 발생했다"는 사실뿐입니다. 구체적으로 무엇이 잘못되었는지 알아내는 유일한 방법은 브라우저의 콘솔(console) 창을 확인하는 것뿐입니다.
다음 섹션들에서는 여러 시나리오를 논의하고, 사용되는 HTTP 헤더들을 하나씩 분석해 보겠습니다.
교차 출처 리소스 공유가 어떻게 동작하는지 보여주는 세 가지 시나리오를 소개합니다. 모든 예제는 이를 지원하는 모든 브라우저에서 교차 출처 요청을 보낼 수 있는 fetch()를 사용합니다.
어떤 요청들은 CORS 사전 요청(preflight)을 트리거하지 않습니다. 현재 CORS를 정의하고 있는 Fetch 명세에서는 해당 용어를 사용하지 않지만, 과거의 CORS 명세에서는 이를 단순 요청(simple requests)이라고 불렀습니다.
이러한 예외가 생긴 이유는, 교차 사이트 fetch()나 XMLHttpRequest가 생기기 훨씬 전부터 존재했던 HTML 4.0의 <form> 요소가 이미 어떤 출처로든 단순 요청을 제출할 수 있었기 때문입니다. 따라서 서버를 작성하는 사람은 이미 교차 사이트 요청 위조 (CSRF) 공격에 대해 방어를 하고 있어야만 했습니다. 이러한 가정하에, 평범한 HTML 폼(form) 제출처럼 보이는 요청이라면 서버가 이를 받기 위해 (사전 요청에 응답하여) 굳이 명시적으로 동의(opt-in)할 필요가 없습니다. CSRF의 위협 측면에서 일반적인 폼 제출보다 더 위험할 것이 없기 때문이죠. 하지만, 서버가 해당 응답을 스크립트(JavaScript)와 공유하도록 허용하려면 여전히 Access-Control-Allow-Origin을 사용하여 명시적으로 동의해야만 합니다.
단순 요청은 다음 조건을 모두 충족하는 요청입니다:
Connection, User-Agent 또는 금지된 요청 헤더들 등)을 제외하고, 사용자가 수동으로 설정할 수 있는 헤더는 오직 CORS 안전 목록에 있는 요청 헤더들 (CORS-safelisted request-headers)뿐일 것:AcceptAccept-LanguageContent-LanguageContent-Type (아래의 추가 요구사항 확인)Range (오직 단일 범위 헤더 값만 허용; 예: bytes=256- 또는 bytes=127-255)Content-Type 헤더에 지정된 미디어 타입(MIME type)에 대해 다음 타입/서브타입 조합들만 허용됨:application/x-www-form-urlencodedmultipart/form-datatext/plain💡 강사의 팁: 실무에서 클라이언트 상태 관리나 데이터 페칭을 위해
fetch로 백엔드와 통신할 때 주로 어떤 데이터 포맷을 쓰시나요? 십중팔구 JSON일 것입니다! 즉, 우리가 흔히 쓰는Content-Type: application/json은 위의 '단순 요청 조건'에 포함되지 않습니다. 따라서 우리가 작성하는 대부분의 API 요청은 무조건 '사전 요청(Preflight)' 과정을 거친다는 것을 꼭 기억하세요! (면접에서도 정말 자주 물어보는 함정입니다.)
XMLHttpRequest 객체를 사용하여 만들어진 경우, 요청에 사용된 XMLHttpRequest.upload 속성이 반환하는 객체에 어떠한 이벤트 리스너도 등록되지 않아야 함.ReadableStream 객체도 사용되지 않아야 함.참고 (Note):
WebKit Nightly와 Safari Technology Preview는Accept,Accept-Language,Content-Language헤더에 허용되는 값들에 대해 추가적인 제한을 둡니다. 만약 이 헤더들 중 하나라도 "비표준(nonstandard)" 값을 가지고 있다면, WebKit/Safari는 해당 요청을 "단순 요청"으로 간주하지 않습니다. 어떤 값을 "비표준"으로 보는지에 대해서는 특정 WebKit 버그 리포트 외에는 공식적으로 문서화되어 있지 않습니다.
다른 브라우저들은 명세의 일부가 아니기 때문에 이러한 추가적인 제한을 구현하지 않습니다.
예를 들어, https://foo.example의 웹 콘텐츠가 https://bar.other 도메인으로부터 JSON 콘텐츠를 가져오고 싶어 한다고 가정해 봅시다. foo.example에 배포된 JavaScript에는 다음과 같은 코드가 사용될 수 있습니다:
const fetchPromise = fetch("[https://bar.other](https://bar.other)");
fetchPromise
.then((response) => response.json())
.then((data) => {
console.log(data);
});
이 작업은 클라이언트와 서버 간에 단순한 정보 교환을 수행하며, 권한을 처리하기 위해 CORS 헤더를 사용합니다:
이 경우 브라우저가 서버로 무엇을 보내는지 살펴보겠습니다:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: [https://foo.example](https://foo.example)
주목할 만한 요청 헤더는 Origin으로, 이 요청이 https://foo.example에서 오고 있음을 나타냅니다.
이제 서버가 어떻게 응답하는지 보겠습니다:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[…XML Data…]
서버는 응답으로 Access-Control-Allow-Origin: * 값을 가진 Access-Control-Allow-Origin 헤더를 반환합니다. 여기서 *는 해당 리소스가 모든(any) 출처에서 접근 가능하다는 것을 의미합니다.
Access-Control-Allow-Origin: *
Origin과 Access-Control-Allow-Origin 헤더를 사용하는 이 패턴이 접근 제어 프로토콜의 가장 단순한 사용법입니다. 만약 https://bar.other의 리소스 소유자가 오직 https://foo.exampleからの 요청에만 접근을 제한하고 싶다면 (즉, https://foo.example 이외의 다른 도메인은 교차 출처 방식으로 리소스에 접근할 수 없게 하려면), 다음과 같이 응답해야 합니다:
Access-Control-Allow-Origin: [https://foo.example](https://foo.example)
참고 (Note):
자격 증명을 포함한 요청 (credentialed requests)에 응답할 때, 서버는Access-Control-Allow-Origin헤더 값으로 와일드카드*를 지정해서는 절대 안 되며(must not), 반드시 명시적인 출처(origin)를 지정해야 합니다.
단순 요청과 달리, "사전 요청(preflighted)"이 필요한 요청의 경우, 브라우저는 실제 요청을 보내는 것이 안전한지 결정하기 위해 먼저 다른 출처의 리소스로 OPTIONS 메서드를 사용한 HTTP 요청을 보냅니다. 이러한 교차 출처 요청은 사용자 데이터에 영향을 미칠 수 있기 때문에 미리 사전 요청을 보내 안전을 확인하는 것입니다.
다음은 사전 요청이 발생하게 될 요청의 예시입니다:
const fetchPromise = fetch("[https://bar.other/doc](https://bar.other/doc)", {
method: "POST",
mode: "cors",
headers: {
"Content-Type": "text/xml",
"X-PINGOTHER": "pingpong",
},
body: "<person><name>Arun</name></person>",
});
fetchPromise.then((response) => {
console.log(response.status);
});
위의 예제는 POST 요청과 함께 보낼 XML 본문을 생성합니다. 또한 비표준 HTTP 요청 헤더인 X-PINGOTHER가 설정되어 있습니다. 이러한 사용자 정의 헤더는 HTTP/1.1 표준의 일부는 아니지만, 웹 애플리케이션에서 일반적으로 유용하게 쓰입니다. 이 요청은 Content-Type이 text/xml이고 커스텀 헤더가 세팅되어 있기 때문에 '사전 요청(preflight)' 과정을 거치게 됩니다.
💡 강사의 팁: 브라우저 개발자 도구(Network 탭)를 열고 API 요청을 살펴보면, 내가 코드에서 보낸 적 없는
OPTIONS라는 메서드의 요청이 하나 더 날아가는 걸 보실 수 있을 거예요. 브라우저가 여러분의 코드를 보호하기 위해 알아서 몰래 먼저 보내는 보안 검사관 같은 거라고 생각하시면 됩니다!
참고 (Note):
아래에서 설명하겠지만, 실제POST요청은Access-Control-Request-*헤더들을 포함하지 않습니다. 이 헤더들은 오직OPTIONS사전 요청 시에만 필요합니다.
클라이언트와 서버 간의 전체 통신 과정을 살펴보겠습니다. 첫 번째 교환은 사전 요청/응답(preflight request/response)입니다:
OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: [https://foo.example](https://foo.example)
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-pingother
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: [https://foo.example](https://foo.example)
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
위의 첫 번째 블록은 OPTIONS 메서드를 사용한 사전 요청을 나타냅니다. 브라우저는 자바스크립트 코드 스니펫에서 사용 중인 요청 파라미터들을 바탕으로 이 사전 요청을 보내야겠다고 스스로 판단합니다. 이를 통해 서버가 실제 요청 파라미터로 요청을 보내는 것을 수락할지 여부를 응답할 수 있게 합니다. OPTIONS는 서버로부터 추가 정보를 알아내기 위해 사용되는 HTTP/1.1 메서드이며, 리소스를 변경할 수 없는 안전한(safe) 메서드입니다. OPTIONS 요청과 함께 두 가지 다른 요청 헤더가 전송되는 것을 주목하세요:
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-pingother
Access-Control-Request-Method 헤더는 사전 요청의 일부로서, 나중에 실제 요청을 보낼 때 POST 요청 메서드를 사용할 것임을 서버에 알려줍니다. Access-Control-Request-Headers 헤더는 실제 요청 시 X-PINGOTHER와 Content-Type 커스텀 헤더를 사용할 것임을 서버에 알려줍니다. 이제 서버는 이러한 조건하에서 요청을 수락할 수 있는지 결정할 기회를 갖게 됩니다.
위의 두 번째 블록은 서버가 돌려주는 응답으로, 요청 메서드(POST)와 요청 헤더(X-PINGOTHER)를 수락한다는 것을 나타냅니다. 다음 줄들을 더 자세히 살펴보겠습니다:
Access-Control-Allow-Origin: [https://foo.example](https://foo.example)
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
서버는 Access-Control-Allow-Origin: https://foo.example으로 응답하여 해당 리소스에 대한 접근을 오직 요청을 보낸 도메인으로 제한합니다. 또한 Access-Control-Allow-Methods를 응답하여 해당 리소스를 조회할 때 POST와 GET이 유효한 메서드임을 알려줍니다 (이 헤더는 Allow 응답 헤더와 유사하지만, 오직 접근 제어(CORS) 컨텍스트 내에서 엄격하게 사용됩니다).
서버는 또한 Access-Control-Allow-Headers를 X-PINGOTHER, Content-Type 값으로 보내어, 이것들이 실제 요청에 사용되도록 허락받은 헤더임을 확인해 줍니다. Access-Control-Allow-Methods와 마찬가지로 Access-Control-Allow-Headers 역시 허용되는 헤더들을 쉼표로 구분한 목록입니다.
마지막으로, Access-Control-Max-Age는 이 사전 요청의 응답 결과가 또 다른 사전 요청을 보내지 않고 캐시(유지)될 수 있는 시간을 초 단위로 알려줍니다. 기본값은 5초입니다. 현재 예제에서는 최대 수명(max age)이 86400초(= 24시간)입니다. 각 브라우저는 이 Access-Control-Max-Age 값이 내부 최댓값을 초과할 경우를 대비하여 브라우저 자체의 최대 내부 값(maximum internal value)을 우선순위로 두고 적용한다는 점을 참고하세요.
사전 요청이 모두 끝나면, 비로소 진짜(real) 요청이 전송됩니다:
POST /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: [https://foo.example/examples/preflightInvocation.html](https://foo.example/examples/preflightInvocation.html)
Content-Length: 55
Origin: [https://foo.example](https://foo.example)
Pragma: no-cache
Cache-Control: no-cache
<person><name>Arun</name></person>
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: [https://foo.example](https://foo.example)
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
[Some XML content]
현재 모든 브라우저가 사전 요청(preflight) 이후에 일어나는 리다이렉트(redirect)를 지원하는 것은 아닙니다. 만약 사전 요청 이후에 서버에서 리다이렉트를 시킨다면, 일부 브라우저는 다음과 같은 에러 메시지를 뿜어냅니다:
The request was redirected to
https://example.com/foo, which is disallowed for cross-origin requests that require preflight.
Request requires preflight, which is disallowed to follow cross-origin redirects.
원래 CORS 프로토콜은 이런 제한적인 동작을 요구했지만, 이후에는 더 이상 이를 강제하지 않도록 명세가 수정되었습니다. 하지만 아직 모든 브라우저가 이 변경 사항을 구현한 것은 아니기 때문에, 여전히 원래 요구되었던(에러를 내는) 동작을 보일 수 있습니다.
모든 브라우저가 최신 명세를 반영할 때까지, 개발자는 다음 중 하나 혹은 두 가지 방법을 사용하여 이 한계를 우회할 수 있습니다:
만약 이 방법들이 불가능하다면, 다음과 같은 우회 방법을 사용할 수도 있습니다:
1. 먼저 (사전 요청이 없는) 단순 요청을 보내어 (Fetch API의 Response.url이나 XMLHttpRequest.responseURL을 사용하여) 리다이렉트가 끝나는 최종 목적지 URL을 알아냅니다.
2. 알아낸 최종 URL을 타겟으로 하여, 첫 번째 단계에서 얻은 주소로 진짜(사전 요청이 필요한) 요청을 보냅니다.
하지만 만약 해당 요청이 Authorization 헤더의 존재 때문에 사전 요청을 유발하는 것이라면, 위에서 설명한 단계들로는 이 한계를 우회할 수 없습니다. 요청을 받는 목적지 서버의 제어 권한을 가지고 있지 않는 이상 이 상황을 아예 우회할 수 없게 됩니다.
참고 (Note):
다른 도메인으로 자격 증명 요청(credentialed requests)을 보낼 때, 서드파티(third-party) 쿠키 정책은 여전히 동일하게 적용됩니다. 이 정책은 이 챕터에서 설명하는 서버와 클라이언트의 설정과 무관하게 항상 강제됩니다.
fetch()나 XMLHttpRequest 그리고 CORS가 제공하는 가장 흥미로운 기능 중 하나는 바로 HTTP 쿠키나 HTTP 인증 정보를 인식하는 "자격 증명(credentialed)" 요청을 보낼 수 있다는 점입니다. 기본적으로 교차 출처(cross-origin) fetch()나 XMLHttpRequest 호출 시, 브라우저는 자격 증명(쿠키 등)을 보내지 않습니다.
fetch() 요청에 자격 증명을 포함하도록 요청하려면, credentials 옵션을 "include"로 설정해야 합니다.
XMLHttpRequest 요청에 자격 증명을 포함하도록 요청하려면, XMLHttpRequest.withCredentials 속성을 true로 설정해야 합니다.
이 예제에서는, 원래 https://foo.example에서 로드된 웹 콘텐츠가 쿠키를 설정하는 https://bar.other의 리소스로 GET 요청을 보냅니다. foo.example의 콘텐츠 안에는 다음과 같은 JavaScript가 포함될 수 있습니다:
const url = "[https://bar.other/resources/credentialed-content/](https://bar.other/resources/credentialed-content/)";
const request = new Request(url, { credentials: "include" });
const fetchPromise = fetch(request);
fetchPromise.then((response) => console.log(response));
이 코드는 생성자(constructor)에서 credentials 옵션을 "include"로 설정하여 Request 객체를 만들고, 이를 fetch()에 넘겨줍니다. 이 요청은 단순(simple) GET 요청이므로 사전 요청(preflight) 과정을 거치지 않습니다. 하지만 브라우저는 응답 헤더에 값이 true인 Access-Control-Allow-Credentials 헤더가 포함되어 있지 않다면 이 응답을 거부(reject)하고, 해당 응답을 호출한 웹(JavaScript) 코드에 제공하지 않습니다.
💡 강사의 팁: 리액트나 넥스트 환경에서 로그인 유지 등을 위해 브라우저의 쿠키(세션 ID 등)를 타 도메인의 백엔드 서버로 보내야 할 때가 있죠. 이때 프론트엔드에서는
credentials: 'include'설정만 하면 끝나는 게 아닙니다. 백엔드 개발자분께 "CORS 응답 헤더에 Access-Control-Allow-Credentials를 true로 설정해 주시고, Origin도*대신 명확한 저희 프론트엔드 도메인으로 지정해 주세요!"라고 당당하게 요청하셔야 안전하게 인증 데이터가 오갈 수 있습니다!
클라이언트와 서버 간의 샘플 통신 내용입니다:
GET /resources/credentialed-content/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Referer: [https://foo.example/examples/credential.html](https://foo.example/examples/credential.html)
Origin: [https://foo.example](https://foo.example)
Cookie: pageAccess=2
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2
Access-Control-Allow-Origin: [https://foo.example](https://foo.example)
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
[text/plain content]
비록 요청의 Cookie 헤더에 https://bar.other의 콘텐츠를 목적지로 하는 쿠키가 포함되어 있다 하더라도, 만약 이 예제에서 보여주는 것처럼 bar.other 서버가 true 값을 가진 Access-Control-Allow-Credentials 헤더를 반환하지 않았다면 브라우저는 해당 응답을 무시하고 웹 콘텐츠(JS)에 응답을 노출하지 않았을 것입니다.
CORS 사전 요청(preflight)은 절대 자격 증명을 포함해서는 안 됩니다. 사전 요청에 대한 응답은 실제 요청에 자격 증명을 포함해서 보낼 수 있음을 나타내기 위해 Access-Control-Allow-Credentials: true를 명시해야 합니다.
참고 (Note):
일부 엔터프라이즈 인증 서비스들은 Fetch 명세에 반하여 사전 요청 시에 TLS 클라이언트 인증서를 전송하도록 요구하기도 합니다.Firefox 87 버전에서는
network.cors_preflight.allow_client_cert환경설정을true로 세팅하여 이러한 비표준 동작을 활성화할 수 있습니다 (Firefox 버그 1511151). Chromium 기반 브라우저들은 현재 CORS 사전 요청 시 항상 TLS 클라이언트 인증서를 전송합니다 (Chrome 버그 775438).
자격 증명을 포함한 요청에 응답할 때:
Access-Control-Allow-Origin 응답 헤더 값으로 와일드카드 *를 지정해서는 절대 안 되며, 대신 명시적인 출처를 지정해야 합니다; 예: Access-Control-Allow-Origin: https://example.comAccess-Control-Allow-Headers 응답 헤더 값으로 와일드카드 *를 지정해서는 절대 안 되며, 대신 명시적인 헤더 이름 목록을 지정해야 합니다; 예: Access-Control-Allow-Headers: X-PINGOTHER, Content-TypeAccess-Control-Allow-Methods 응답 헤더 값으로 와일드카드 *를 지정해서는 절대 안 되며, 대신 명시적인 메서드 이름 목록을 지정해야 합니다; 예: Access-Control-Allow-Methods: POST, GETAccess-Control-Expose-Headers 응답 헤더 값으로 와일드카드 *를 지정해서는 절대 안 되며, 대신 명시적인 헤더 이름 목록을 지정해야 합니다; 예: Access-Control-Expose-Headers: Content-Encoding, Kuma-Revision요청이 자격 증명(대부분 Cookie 헤더)을 포함하고 있는데 응답에 (와일드카드가 포함된) Access-Control-Allow-Origin: * 헤더가 포함되어 있다면, 브라우저는 응답에 대한 접근을 차단하고 개발자 도구 콘솔에 CORS 에러를 보고합니다.
하지만 요청에 자격 증명(예: Cookie 헤더)이 포함되어 있고 응답에도 와일드카드가 아닌 실제 출처(예: Access-Control-Allow-Origin: https://example.com)가 명시되어 있다면, 브라우저는 지정된 출처가 해당 응답에 접근하는 것을 허용합니다.
또한, 만약 응답의 Access-Control-Allow-Origin 값이 실제 출처가 아닌 와일드카드 *라면, 응답 내에 있는 그 어떤 Set-Cookie 응답 헤더도 쿠키를 성공적으로 저장(set)할 수 없다는 점도 명심하세요.
CORS 응답에서 설정된 쿠키들은 일반적인 서드파티 쿠키(third-party cookie) 정책의 적용을 받습니다. 위 예제에서 페이지는 foo.example에서 로드되었지만 응답의 Set-Cookie 헤더는 bar.other에 의해 전송되었습니다. 만약 사용자의 브라우저가 모든 서드파티 쿠키를 거부하도록 설정되어 있다면, 이 쿠키는 저장되지 않을 것입니다.
CORS 요청 및 응답에서 설정된 쿠키는 정상적인 서드파티 쿠키 정책의 적용을 받습니다.
서드파티 쿠키 정책은 서드파티 쿠키가 요청에 포함되어 전송되는 것을 막을 수 있으며, 이는 서버가 (Access-Control-Allow-Credentials를 사용하여) 허락했음에도 불구하고 사이트가 자격 증명 요청을 보내는 것을 원천적으로 차단하는 효과를 낳습니다. 브라우저마다 기본 정책은 다르지만, SameSite 속성을 통해 이를 조정할 수 있습니다.
설령 자격 증명을 포함한 요청이 허용되더라도, 브라우저가 응답으로 들어오는 모든 서드파티 쿠키를 거부하도록 설정되어 있을 수 있습니다.
이 섹션은 교차 출처 리소스 공유(CORS) 명세에 정의된 바에 따라, 접근 제어 요청에 대해 서버가 반환하는 HTTP 응답 헤더들을 나열합니다. 이전 섹션에서 이것들이 어떻게 작동하는지 개요를 보여드렸습니다.
반환되는 리소스는 다음과 같은 구문을 가진 하나의 Access-Control-Allow-Origin 헤더를 가질 수 있습니다:
Access-Control-Allow-Origin: <origin> | *
Access-Control-Allow-Origin은 브라우저에게 리소스 접근을 허락할 단일 출처(origin)를 지정하거나, 자격 증명이 없는 요청에 한하여 와일드카드 *를 사용해 모든 출처의 접근을 허용할 수 있습니다.
예를 들어, https://mozilla.org 출처의 코드가 리소스에 접근하는 것을 허용하려면 다음과 같이 지정합니다:
Access-Control-Allow-Origin: [https://mozilla.org](https://mozilla.org)
Vary: Origin
만약 서버가 와일드카드 * 대신 단일 출처(허용 목록(allowlist)에 기반하여 요청한 출처에 따라 동적으로 바뀔 수 있음)를 지정한다면, 서버는 클라이언트에게 "서버의 응답이 Origin 요청 헤더 값에 따라 달라질 수 있다"는 것을 알리기 위해 반드시 Vary 응답 헤더에 Origin을 포함시켜야 합니다.
Access-Control-Expose-Headers 헤더는 브라우저의 JavaScript(예를 들어 Response.headers)가 접근할 수 있도록 허용하는 헤더들의 목록을 허용 목록(allowlist)에 추가합니다.
Access-Control-Expose-Headers: <header-name>[, <header-name>]*
예를 들어, 다음과 같이 지정하면 브라우저가 X-My-Custom-Header와 X-Another-Custom-Header 헤더에 접근할 수 있게 노출해 줍니다:
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
Access-Control-Max-Age 헤더는 사전 요청(preflight request)의 결과를 얼마나 오랫동안 캐시할 수 있는지 나타냅니다. 사전 요청의 예제는 앞서 다룬 내용들을 참고하세요.
Access-Control-Max-Age: <delta-seconds>
delta-seconds 파라미터는 결과를 캐시할 수 있는 초 단위의 시간을 나타냅니다.
Access-Control-Allow-Credentials 헤더는 credentials 플래그가 참(true)일 때 해당 요청에 대한 응답을 노출해도 되는지를 나타냅니다. 사전 요청에 대한 응답의 일부로 사용될 경우, 이는 실제 요청이 자격 증명을 사용해서 보내질 수 있는지 여부를 나타냅니다. 참고로 단순(simple) GET 요청은 사전 요청을 거치지 않으므로, 자격 증명과 함께 리소스를 요청했을 때 이 헤더가 리소스와 함께 반환되지 않는다면 브라우저는 그 응답을 무시하고 웹 콘텐츠(JS)에 반환하지 않습니다.
Access-Control-Allow-Credentials: true
자격 증명을 포함한 요청에 대한 자세한 내용은 앞선 섹션을 참고하세요.
Access-Control-Allow-Methods 헤더는 리소스에 접근할 때 허용되는 메서드 하나 또는 여러 개를 지정합니다. 이 헤더는 사전 요청에 대한 응답으로 사용됩니다. 어떤 조건에서 요청이 사전 요청되는지에 대해서는 앞서 설명했습니다.
Access-Control-Allow-Methods: <method>[, <method>]*
사전 요청의 예시와 이 헤더를 브라우저로 보내는 예제는 앞선 섹션에서 찾아볼 수 있습니다.
Access-Control-Allow-Headers 헤더는 실제 요청을 보낼 때 어떤 HTTP 헤더들을 사용할 수 있는지 알려주기 위해 사전 요청에 대한 응답으로 사용됩니다. 이 헤더는 브라우저가 보내는 Access-Control-Request-Headers 헤더에 대한 서버 측의 짝이 되는 응답입니다.
Access-Control-Allow-Headers: <header-name>[, <header-name>]*
이 헤더의 사용 예시는 위에서 찾아볼 수 있습니다.
이 섹션은 교차 출처 공유 기능을 사용하기 위해 HTTP 요청을 보낼 때 클라이언트가 사용할 수 있는 헤더들을 나열합니다. 참고로 이 헤더들은 서버로 요청을 보낼 때 브라우저가 알아서 자동으로 설정해 주는 것들입니다. 교차 출처 요청을 개발하는 개발자가 프로그램 코드 단에서 이런 교차 출처 공유 관련 요청 헤더들을 직접 수동으로 설정할 필요는 없습니다.
Origin 헤더는 교차 출처 접근 요청이나 사전 요청이 어디에서 출발했는지를 나타냅니다.
Origin: <origin>
origin은 요청이 시작된 서버를 나타내는 URL입니다. 이 URL에는 경로(path) 정보가 포함되지 않으며 오로지 서버의 이름(스킴, 호스트, 포트)만 포함됩니다.
참고 (Note):
origin의 값은null이 될 수도 있습니다.
모든 접근 제어 요청(CORS 요청)에는 Origin 헤더가 항상 전송된다는 사실을 명심하세요.
Access-Control-Request-Method는 사전 요청(preflight)을 보낼 때, 나중에 이어질 실제 요청에서 어떤 HTTP 메서드를 사용할 것인지 서버에 미리 알려주기 위해 사용됩니다.
Access-Control-Request-Method: <method>
이 헤더의 사용 예시는 위에서 확인할 수 있습니다.
Access-Control-Request-Headers 헤더는 사전 요청(preflight)을 보낼 때, 나중에 이어질 실제 요청에서 어떤 HTTP 헤더들을 사용할 것인지(예를 들어, Fetch API의 headers 옵션을 통해 전달한 커스텀 헤더 등)를 서버에 미리 알려주기 위해 사용됩니다. 브라우저가 보내는 이 요청 헤더에 대해 서버는 Access-Control-Allow-Headers 응답 헤더로 허락 여부를 대답하게 됩니다.
Access-Control-Request-Headers: <field-name>[,<field-name>]*
이 헤더의 사용 예시는 위에서 확인할 수 있습니다.
| 명세 (Specification) |
|---|
| Fetch - #http-access-control-allow-origin |
XMLHttpRequest