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를 광범위하게 크로스 오리진으로 개방
안전한 설정
- 허용 목록(고정된 신뢰 오리진만)
- 그 외 모두에 대한 기본 거부
- 자격 증명 요청에는
*가 아닌 특정 오리진
설정 오류는 대개 이렇게 생긴다
설정 오류의 대부분은 CORS 오류를 없애려다 생깁니다. 전형적인 흐름은 다음과 같습니다.
- 프런트엔드에서 쿠키를 실어 API를 호출하니 CORS 오류가 난다
Access-Control-Allow-Origin: *를 추가한다. 하지만 자격 증명이 있는 요청에서는 브라우저가*응답을 페이지에서 읽지 못하게 하므로 오류가 남는다- 들어온
Origin을 그대로 되돌려주도록 바꾸자 오류가 사라진다. 동시에 어떤 사이트든 로그인된 응답을 읽을 수 있게 된다
허용 목록을 쓰더라도 비교가 허술하면 같은 결과가 됩니다.
- "
example.com으로 끝나는가"로 비교하면evil-example.com같은 다른 도메인도 통과한다 - "
https://example.com으로 시작하는가"로 비교하면https://example.com.evil.net도 통과한다 - 개발 편의를 위해
null오리진을 허용하면, 다른 사이트가 만들어 낼 수 있는null오리진에서의 읽기도 허용된다
오리진 전체(스킴, 호스트, 포트)를 완전 일치로 비교하세요.
안전한 설정의 기본
허용 목록 사용(가장 중요)
Origin을 무분별하게 반사하지 않기
Origin을 Access-Control-Allow-Origin에 그대로 되돌려주지 마세요. 허용 목록과 일치할 때만 반환하세요. 값이 요청마다 달라지므로 캐시가 응답을 섞지 않도록 Vary: Origin도 함께 보내세요.* 와 자격 증명을 절대 병용하지 않기
Access-Control-Allow-Origin: *를 Allow-Credentials: true와 함께 쓰지 마세요. 대신 특정 오리진을 명시하세요.여는 범위를 최소화
*는 로그인이 필요 없는 공개 데이터에만 쓰세요.내 사이트에서 확인하는 방법
다른 오리진을 자칭하는 요청을 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