CORS 자격 증명 허용 시 동적 Origin

CORS Misconfiguration (Credentials + Dynamic Origin)

설명

Access-Control-Allow-Credentials: true와 함께 요청의 Origin이나 다른 사용자 입력을 검증 없이 Access-Control-Allow-Origin에 반영하면, 신뢰하지 않는 출처가 응답을 읽을 수 있습니다. 자격 증명 전송은 클라이언트의 요청 옵션과 쿠키의 SameSite·서드파티 정책에도 달려 있습니다. 이 조건에서 악성 출처의 스크립트가 인증된 응답을 읽으면 민감 정보가 노출될 수 있습니다.

CORS는 브라우저의 응답 열람을 통제하며 서버 인증·인가나 CSRF 방어를 대신하지 않습니다. CORS 헤더가 없어도 일부 교차 출처 요청은 서버에 도달할 수 있습니다.

잠재적 영향

  • 인증된 응답 노출: 자격 증명이 전송된 요청의 응답을 악성 출처에서 읽어 민감 정보를 얻을 수 있습니다.
  • 무단 상태 변경: 서버의 인증·인가나 CSRF 방어도 부족하면 사용자의 권한으로 데이터를 변경할 수 있습니다.
  • 다른 접근 제어 오류와 결합: IDOR 같은 문제로 허용되지 않은 리소스까지 조회·수정할 수 있는 경우 피해가 커집니다.
  • 비공개 응답의 외부 공개: 브라우저가 도달할 수 있는 내부 API의 응답도 잘못된 출처에 노출될 수 있습니다.

해결 방법

  • 엄격한 allowlist: 허용할 Origin을 사전에 정확히 나열(Set/Map)하고, 목록에 일치하는 경우에만 Access-Control-Allow-Origin을 해당 Origin으로 설정하세요. 정규식 사용 시 스킴/호스트/포트를 명확히 고정하세요.
  • 절대 반영 금지: 요청의 Origin/쿼리/헤더 값을 그대로 반영하지 마세요. Access-Control-Allow-Origin: "null"도 사용하지 마세요.
  • 자격 증명 조건부 허용: allowlist에 일치할 때만 Access-Control-Allow-Credentials: true를 설정하고, 불일치 시 이 헤더를 보내지 않거나 403으로 거절하세요.
  • Vary: Origin 설정: 여러 합법 Origin을 지원하는 경우, 캐시 오동작을 막기 위해 Vary: Origin을 함께 설정하세요.
  • 프리플라이트(OPTIONS) 검증: 허용된 메서드/헤더만 Access-Control-Allow-Methods/Headers로 응답하고, 비허용 조합은 거절하세요.
  • 쿠키 보강: SameSite로 교차 사이트 쿠키 전송을 제한하고, Secure와 HttpOnly로 전송 경로와 스크립트 접근을 보호하세요. 민감 작업에는 CSRF 토큰 등 별도 검증을 적용하세요.

예시

CORS 헤더 설정을 비교하며 실제 인증·인가와 CSRF 처리는 생략했습니다.

변경 전

javascript
const express = require("express");
const app = express();

// 취약: 요청 Origin/쿼리 값을 그대로 반영하고 credentials를 항상 허용
app.use((req, res, next) => {
  const o = req.get("Origin") || req.query.o || "null";
  res.set("Access-Control-Allow-Origin", o);
  res.set("Access-Control-Allow-Credentials", "true");
  next();
});

app.get("/profile", (req, res) => {
  res.json({ email: "user@example.com" });
});

변경 후

javascript
const express = require("express");
const app = express();

// 안전: 엄격한 allowlist 기반의 조건부 설정 + Vary: Origin
const ALLOWLIST = new Set([
  "https://app.example.com",
  "https://admin.example.com",
]);

app.use((req, res, next) => {
  const origin = req.get("Origin");
  if (origin && ALLOWLIST.has(origin)) {
    res.set("Access-Control-Allow-Origin", origin);
    res.set("Access-Control-Allow-Credentials", "true");
    res.set("Vary", "Origin");
  } else {
    // 불일치 시 CORS 자격 증명 미허용 및 헤더 미설정(또는 403 반환)
    // res.status(403).end(); // 정책에 따라 선택
  }
  next();
});

// 프리플라이트 예시
app.options("/profile", (req, res) => {
  const origin = req.get("Origin");
  if (origin && ALLOWLIST.has(origin)) {
    res.set("Access-Control-Allow-Origin", origin);
    res.set("Access-Control-Allow-Credentials", "true");
    res.set("Access-Control-Allow-Methods", "GET");
    res.set("Access-Control-Allow-Headers", "Content-Type");
    res.set("Vary", "Origin");
    return res.sendStatus(204);
  }
  return res.sendStatus(403);
});

app.get("/profile", (req, res) => {
  res.json({ email: "user@example.com" });
});

설명:

  • 변경 전: 검증하지 않은 출처를 반영하고 자격 증명 응답을 허용합니다. 요청 옵션과 쿠키 정책이 전송을 허용하면 악성 출처에서 인증된 응답을 읽을 수 있습니다.
  • 변경 후: 허용 목록과 정확히 일치하는 출처에만 응답 열람을 허용하고 프리플라이트에도 같은 목록을 적용합니다. 헤더를 생략한다고 요청 자체나 쿠키 전송이 모두 차단되는 것은 아닙니다. Vary: Origin은 출처에 따라 응답이 달라짐을 캐시에 알립니다.

참조