- 위험도
- 높음(High)
- CWE
- CWE-942 · 교차출처 권한 과다 허용
- OWASP
- A05:2021
- 룰
- vg-cors-wildcard-credentials · vg-cors-reflect-origin
이게 무슨 말이냐 (쉽게)
브라우저에는 원래 "A사이트의 자바스크립트가 B사이트 데이터를 마음대로 읽지 못하게" 막는 담이 있습니다. CORS는 그 담에 당신이 직접 내주는 출입증이에요.
- Access-Control-Allow-Origin: * = 아무 사이트나 들어와도 좋다
- Access-Control-Allow-Credentials: true = 들어올 때 그 사람 쿠키(로그인 상태)도 같이 가져와라
둘을 함께 켜면 "아무 사이트에서나, 우리 회원의 로그인 상태로" API를 부를 수 있게 됩니다. 브라우저 표준은 이 조합 자체는 거부하는데, 서버가 * 대신 요청이 보낸 Origin을 그대로 되돌려주면 실질적으로 같은 상태가 됩니다. 그게 두 번째 룰(vg-cors-reflect-origin)이고, 사실 *보다 위험해요 — 표준이 걸어둔 브레이크까지 풀리거든요.
실제로 어떻게 터지나 (시나리오)
당신 API는 쿠키로 로그인을 유지합니다. 개발 중 CORS 오류가 떠서 검색했고, 나온 답을 그대로 붙였어요.
res.setHeader("Access-Control-Allow-Origin", req.headers.origin); // 요청 Origin을 반사
res.setHeader("Access-Control-Allow-Credentials", "true");오류는 사라졌고, 그대로 배포했습니다. 공격자가 쿠폰 이벤트처럼 보이는 페이지를 만들어 링크를 뿌립니다. 당신 서비스에 로그인해 둔 사용자가 그 링크를 열면, 페이지의 스크립트가 조용히 fetch("https://내도메인/api/me", { credentials: "include" })를 실행해요. 서버는 그 Origin을 그대로 허가해 주고, 쿠키가 붙어 있으니 본인 확인까지 통과합니다. 이름·이메일·주소가 공격자 서버로 넘어갑니다. 사용자는 쿠폰 페이지를 봤을 뿐이에요.
최악의 경우
- 로그인 사용자의 개인정보·주문내역·토큰을 조용히 긁어감. 피해자는 알 방법이 없습니다.
- 읽기뿐 아니라 쓰기 API까지 열려 있으면 → 이메일·비밀번호 변경으로 계정 탈취.
- CSRF 방어(룰 vg-csrf-missing)까지 없으면 결제·탈퇴 같은 동작도 대신 실행됩니다.
나한테 해당되나? (체크)
- 프론트와 API가 다른 도메인이다(예: app.내도메인 ↔ api.내도메인, 또는 Vercel ↔ 별도 서버).
- CORS 오류를 없애려고 origin: "*" 또는 origin: true를 넣은 적이 있다.
- credentials: true(또는 Access-Control-Allow-Credentials: true)를 켰다.
- 요청의 req.headers.origin을 그대로 응답 헤더에 넣는다.
- 로그인 상태를 쿠키로 유지한다.
→ 2번이나 4번이 체크되면 해당됩니다.
어떻게 고치나 (복붙)
① 허용할 주소를 목록으로 못 박기 (와일드카드·반사 금지)
const ALLOWED = new Set([
"https://내도메인.com",
"https://app.내도메인.com",
]);
const origin = req.headers.get("origin") || "";
if (ALLOWED.has(origin)) { // 목록에 있을 때만 허가
res.headers.set("Access-Control-Allow-Origin", origin);
res.headers.set("Vary", "Origin"); // 캐시가 다른 사이트에 섞이지 않게
res.headers.set("Access-Control-Allow-Credentials", "true");
}② 라이브러리를 쓸 때도 목록으로
- app.use(cors({ origin: "*", credentials: true })); // 전부 허용
- app.use(cors({ origin: true, credentials: true })); // origin:true = 반사
+ app.use(cors({ origin: ["https://내도메인.com"], credentials: true }));③ 공개 API라 정말 아무나 불러도 되면 — 자격증명을 끄기. Allow-Credentials를 아예 넣지 않고, 쿠키 대신 요청마다 토큰을 받으세요. * 자체가 문제가 아니라 * + 쿠키가 문제입니다.
④ 부분 일치 검사 금지 — origin.includes("내도메인.com")은 내도메인.com.공격자.kr을 통과시킵니다. 반드시 전체 문자열 일치로.
고친 뒤 확인
- 01
남의 출처로 물어보기
허가 헤더가 안 나와야 정상입니다.
curl -sI https://내도메인/api/me -H "Origin: https://evil.example" | grep -i access-control - 02
내 도메인으로 물어보기
Access-Control-Allow-Origin에 내 도메인 하나만 나와야 합니다(*가 아니라).
- 03
캐시 오염 막기
Vary: Origin이 붙었는지 확인 — 없으면 CDN이 A사이트용 응답을 B사이트에 그대로 줄 수 있습니다.
규제 연결
교차출처로 자격증명이 새는 구성이라 ISMS-P 2.6.1(접근통제)·2.10(보안관리), PIPA 제29조 안전조치의무에 걸립니다.
출처/더 읽기: OWASP A05:2021(Security Misconfiguration), CWE-942, MDN CORS 문서(Allow-Origin *와 credentials 조합 제한), PortSwigger Web Security Academy — CORS. 이 카드는 우리가 직접 작성했습니다.
← 문서 목록으로