용어 사전

CORS란 — 작동 원리와 잘못된 설정이 노출하는 것

CORS는 한 사이트의 JavaScript가 다른 오리진의 API 응답을 읽을 수 있는지를 제어합니다. 설정 오류가 로그인된 사용자 데이터를 노출하는 이유와 허용 목록 등 안전한 설정을 설명합니다.

게시 2026-06-11 업데이트 2026-10-06 5분 읽기
이 페이지에서

Access-Control-Allow-Origin을 추가해서 API가 동작한다고 해서 안전한 것은 아닙니다. CORS는 한 번 추가하고 끝나는 것이 아니라, 누구를 허용할지 좁히는 것이 중요합니다. 작동 원리와 안전하게 설정하는 법을 살펴봅니다(공격 절차는 다루지 않습니다).

먼저, 작동 원리

브라우저는 **동일 출처 정책(same-origin policy)**을 강제합니다: 다른 오리진의 JS는 다른 사이트의 응답을 자유롭게 읽을 수 없습니다. CORS는 그 예외를 서버가 명시적으로 허용한 대상에게만 여는 것입니다.

오리진은 스킴(https), 호스트 이름, 포트의 조합입니다. https://example.com과 https://api.example.com은 호스트가 달라서 다른 오리진이고, http://example.com과 https://example.com은 스킴이 달라서 역시 다른 오리진입니다.

응답 헤더의미
Access-Control-Allow-Origin어떤 오리진이 응답을 읽어도 되는가
Access-Control-Allow-Credentials자격 증명(쿠키)이 포함된 요청을 허용하는가
Access-Control-Allow-Methods / -Headers어떤 HTTP 메서드/헤더를 허용하는가

핵심: CORS는 "제한을 푸는" 쪽입니다. 잘못 풀면 읽혀서는 안 될 데이터가 제3자에게 읽히게 됩니다.

하나 더 중요한 점이 있습니다. CORS가 막는 것은 JavaScript가 응답을 읽는 것입니다. 단순 요청은 CORS 허용이 없어도 서버에 도달해 처리됩니다. 사용자 정의 헤더를 쓰는 경우 등에는 브라우저가 먼저 OPTIONS 요청(프리플라이트)으로 확인하고, 허용되지 않으면 실제 요청을 보내지 않습니다.

비슷한 방식과의 차이

CORS는 이름이 비슷한 방식이나 가까운 취약점과 자주 혼동됩니다.

용어정하는 것누가 강제하나
동일 출처 정책다른 오리진은 응답을 읽을 수 없다(브라우저 기본)브라우저
CORS그 예외로서 어떤 오리진이 읽어도 되는가서버가 선언하고 브라우저가 강제
CSRF사용자의 로그인된 브라우저로 의도하지 않은 요청을 보내게 하는 공격CSRF 토큰과 SameSite로 막음
CSP내 페이지가 불러와도 되는 스크립트와 연결 대상서버가 선언하고 브라우저가 강제

CORS는 "응답을 읽을 수 있는가", CSRF는 "사용자 대신 요청을 보낼 수 있는가"의 문제입니다. CORS를 조여도 CSRF는 막히지 않습니다.

무엇이 설정을 위험하게 만드는가

위험한 설정(흔함)

  • 요청의 Origin을 그대로 허용으로 반사
  • Access-Control-Allow-Origin: * 와 자격 증명 허용 병용
  • 필요 없는 API를 광범위하게 크로스 오리진으로 개방

안전한 설정

  • 허용 목록(고정된 신뢰 오리진만)
  • 그 외 모두에 대한 기본 거부
  • 자격 증명 요청에는 *가 아닌 특정 오리진
공격자 사이트의 JS가 내 API에 요청(사용자는 로그인 상태)
↓ 서버가 Origin을 무분별하게 허용(설정 오류)
브라우저가 응답 읽기를 허용 = 개인 데이터/토큰이 공격자 사이트에 도달
느슨한 설정이면 공격자 사이트의 JS가 사용자의 로그인 상태를 이용해 API 응답을 읽을 수 있습니다.

설정 오류는 대개 이렇게 생긴다

설정 오류의 대부분은 CORS 오류를 없애려다 생깁니다. 전형적인 흐름은 다음과 같습니다.

  1. 프런트엔드에서 쿠키를 실어 API를 호출하니 CORS 오류가 난다
  2. Access-Control-Allow-Origin: *를 추가한다. 하지만 자격 증명이 있는 요청에서는 브라우저가 * 응답을 페이지에서 읽지 못하게 하므로 오류가 남는다
  3. 들어온 Origin을 그대로 되돌려주도록 바꾸자 오류가 사라진다. 동시에 어떤 사이트든 로그인된 응답을 읽을 수 있게 된다

허용 목록을 쓰더라도 비교가 허술하면 같은 결과가 됩니다.

  • "example.com으로 끝나는가"로 비교하면 evil-example.com 같은 다른 도메인도 통과한다
  • "https://example.com으로 시작하는가"로 비교하면 https://example.com.evil.net도 통과한다
  • 개발 편의를 위해 null 오리진을 허용하면, 다른 사이트가 만들어 낼 수 있는 null 오리진에서의 읽기도 허용된다

오리진 전체(스킴, 호스트, 포트)를 완전 일치로 비교하세요.

안전한 설정의 기본

1

허용 목록 사용(가장 중요)

미리 정한 신뢰 오리진만 허용하고 나머지는 거부하세요(기본 거부). "그냥 전부 허용"하지 마세요.
2

Origin을 무분별하게 반사하지 않기

요청의 Origin을 Access-Control-Allow-Origin에 그대로 되돌려주지 마세요. 허용 목록과 일치할 때만 반환하세요. 값이 요청마다 달라지므로 캐시가 응답을 섞지 않도록 Vary: Origin도 함께 보내세요.
3

* 와 자격 증명을 절대 병용하지 않기

Access-Control-Allow-Origin: *를 Allow-Credentials: true와 함께 쓰지 마세요. 대신 특정 오리진을 명시하세요.
4

여는 범위를 최소화

정말로 크로스 오리진이 필요한 엔드포인트만 여세요. 광범위하게 열지 마세요. *는 로그인이 필요 없는 공개 데이터에만 쓰세요.

내 사이트에서 확인하는 방법

다른 오리진을 자칭하는 요청을 API에 보내고 응답 헤더를 보세요. 자신이 관리하는 API에만 하세요.

# 허용하지 않은 오리진을 자칭하고, 응답 헤더만 본다
curl -s -o /dev/null -D - -H "Origin: https://example.com" https://your-api.example/ | grep -i "^access-control"
  • Access-Control-Allow-Origin: https://example.com이 돌아오면 API가 Origin을 반사하고 있다. 허용 목록 비교를 추가한다
  • Access-Control-Allow-Origin: *와 Access-Control-Allow-Credentials: true가 함께 나오면 설정을 다시 본다
  • 아무것도 돌아오지 않거나, 자신의 프런트엔드 오리진만 돌아오면 괜찮다

Origin: null로도 같은 확인을 반복하세요. 소유를 확인한 사이트라면 사이트 보안 점검이 오리진 반사와 자격 증명 설정을 함께 검사합니다. 다른 보안 헤더는 보안 헤더 검사기로 확인할 수 있습니다.

본 사이트의 견해: CORS는 '방어'가 아니라 '여는 방식'이다 — 누구에게 열지를 좁혀라

흔한 오해는 "CORS 헤더를 추가하면 안전해진다"입니다. 사실은 그 반대입니다. CORS는 브라우저의 제한을 느슨하게 풉니다. 그래서 중요한 것은 "어떻게 추가하느냐"가 아니라 "누구에게, 어디까지 여느냐를 최소화하는 것"입니다. 본 사이트는 외부에서 읽혀도 되는 데이터와 그렇지 않은 데이터를 분리하고, 기본 거부에 허용 목록으로 예외만 여는 방식을 권장합니다. XSS(읽기 전제를 무너뜨림)와 CSRF(상태 변경을 노림)는 별개의 결함임에 유의하세요 — CORS 하나로 전부를 덮을 수는 없습니다.

출처(1차 자료)

  • MDN Web Docs, "Cross-Origin Resource Sharing (CORS)" — developer.mozilla.org(자격 증명 요청에서 * 불가, 프리플라이트 조건, Vary: Origin)
  • WHATWG Fetch Standard, "CORS protocol" — fetch.spec.whatwg.org
  • OWASP HTML5 Security Cheat Sheet — cheatsheetseries.owasp.org(신뢰하는 도메인만 허용, Origin 헤더를 확인 없이 돌려주지 않기)
  • PortSwigger Web Security Academy, "Cross-origin resource sharing (CORS)" — portswigger.net(허용 목록 비교의 실수, null 오리진, CORS는 CSRF 대책이 아님)
  • MITRE CWE-942: Permissive Cross-domain Policy with Untrusted Domains — cwe.mitre.org

다음으로 읽기

분류 번호

CWE는 취약점의 원인이 되는 약점의 종류를, MITRE ATT&CK는 공격자의 기법을 분류한 번호 체계입니다. 둘 다 MITRE가 관리하며 취약점 정보와 탐지 규칙 분류에 널리 쓰입니다.

CWE™: Copyright © 2006–2026, The MITRE Corporation. CWE is a trademark of The MITRE Corporation. The MITRE Corporation hereby grants you a non-exclusive, royalty-free license to use CWE for research, development, and commercial purposes. Any copy you make for such purposes is authorized on the condition that you reproduce MITRE’s copyright designation and this license in any such copy. CWE Terms of Use

FAQ

QCORS는 무엇을 위한 것인가요?
A

브라우저는 동일 출처 정책(same-origin policy)을 강제합니다: 한 오리진(사이트)의 JavaScript는 다른 사이트의 응답을 자유롭게 읽을 수 없습니다. CORS(Cross-Origin Resource Sharing)는 서버가 그 예외를 — 단, 명시적으로 허용한 대상에게만 — 여는 방식입니다. Access-Control-Allow-Origin 같은 응답 헤더가 어떤 오리진이 응답을 읽어도 되는지를 선언합니다.

QCORS 설정 오류는 무엇을 노출하나요?
A

지나치게 느슨한 권한은 공격자의 사이트가 JavaScript로 사용자의 로그인된 API 응답을 읽게 할 수 있습니다. 전형적인 경우: 요청의 Origin을 그대로 허용으로 반사하거나, 자격 증명(쿠키)도 허용하면서 Access-Control-Allow-Origin을 *로 설정하는 것. 그 결과 개인 데이터나 토큰이 제3자 사이트에 도달할 수 있습니다.

Q안전한 설정의 기준은 무엇인가요?
A

허용 목록(allowlist)입니다. 미리 정한 신뢰 오리진만 허용하고 나머지는 거부하세요(기본 거부). 요청의 Origin을 무분별하게 반사하지 마세요. 자격 증명이 포함된 요청에는 Access-Control-Allow-Origin에 *를 쓰지 말고 특정 오리진을 명시하세요. 그리고 정말로 크로스 오리진이 필요한 엔드포인트만 노출하세요.

QCORS 오류가 나는데, Access-Control-Allow-Origin을 *로 하면 되나요?
A

로그인 없이 공개 데이터만 돌려주는 API라면 *로 괜찮습니다. 하지만 쿠키 같은 자격 증명을 싣는 요청에서는 브라우저가 * 응답을 페이지에서 읽지 못하게 하므로 오류가 남습니다. 그래서 들어온 Origin을 그대로 되돌려주도록 바꾸면 어떤 사이트든 응답을 읽을 수 있게 됩니다. 올바른 해결책은 자신의 프런트엔드 오리진을 허용 목록에 적고, 완전히 일치할 때만 그 값을 돌려주는 것입니다.

QCORS를 설정하면 CSRF를 막을 수 있나요?
A

막을 수 없습니다. CORS가 제어하는 것은 응답을 읽을 수 있는지이지, 요청이 전송되는지가 아닙니다. 단순 요청(폼 제출 형태의 POST 등)은 CORS 허용이 없어도 서버에 도달합니다. CSRF는 CSRF 토큰과 쿠키의 SameSite 속성으로 막으세요.

Q프리플라이트 요청이란 무엇인가요?
A

브라우저가 실제 요청을 보내기 전에, 그 메서드와 헤더가 허용되는지 OPTIONS 요청으로 서버에 묻는 것입니다. 사용자 정의 헤더, application/json의 POST, PUT, DELETE 등에서 일어납니다. 허용되지 않으면 실제 요청은 전송되지 않습니다. 허용된 헤더와 콘텐츠 유형만 쓰는 GET, HEAD, POST 같은 단순 요청에서는 일어나지 않습니다.