본문 바로가기
카테고리 없음

CORS — 네트워크 기초

by SuldenLion 2026. 7. 30.
반응형
CORS — 네트워크 기초

SOP — 동일 출처 정책

CORS를 이해하려면 먼저 SOP(Same-Origin Policy, 동일 출처 정책)를 알아야 한다. SOP는 브라우저의 핵심 보안 모델로, "스크립트는 자신과 같은 출처(Origin)의 리소스에만 접근할 수 있다"는 규칙이다.

출처(Origin)프로토콜 + 도메인 + 포트의 조합이다. 셋 중 하나라도 다르면 다른 출처(Cross-Origin)다.

기준 출처: https://www.example.com:443
✅ 동일 출처 https://www.example.com/api/data 경로만 다름
❌ 다른 출처 http://www.example.com/api 프로토콜 다름 (https vs http)
❌ 다른 출처 https://api.example.com/data 서브도메인 다름
❌ 다른 출처 https://www.example.com:8080 포트 다름
❌ 다른 출처 https://www.other.com/api 도메인 자체가 다름
💡 SOP가 없다면?
evil.com의 스크립트가 사용자의 브라우저에서 bank.com의 API를 호출해 계좌 정보를 탈취할 수 있다. 사용자는 bank.com에 로그인된 상태이므로 쿠키가 자동 전송된다. SOP는 이 공격(CSRF 변형)을 원천 차단한다.

CORS — 교차 출처 리소스 공유

현실에서는 프론트엔드(app.com)와 API 서버(api.com)가 다른 출처인 경우가 많다. CORS(Cross-Origin Resource Sharing)는 SOP의 엄격한 제한을 서버가 명시적으로 허용하는 방식으로 완화하는 메커니즘이다.

중요한 것은 CORS 정책이 브라우저에서 강제된다는 점이다. curl이나 서버-서버 요청에는 CORS가 적용되지 않는다. 브라우저만이 SOP/CORS를 강제하는 주체다.

단순 요청 (Simple Request)

특정 조건을 만족하는 요청은 Preflight 없이 바로 전송된다. 조건: GET/POST/HEAD 메서드, 기본 헤더만 사용, Content-Type이 text/plain·multipart/form-data·application/x-www-form-urlencoded 중 하나.

단순 요청 흐름
1
브라우저가 요청 전송 + Origin 헤더 자동 추가
GET /api/data HTTP/1.1
Origin: https://app.com
Host: api.com
2
서버가 응답 + CORS 헤더 포함
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.com
Content-Type: application/json
3
브라우저가 Access-Control-Allow-Origin을 확인. 허용된 출처면 응답을 스크립트에 전달. 아니면 차단.

Preflight 요청

단순 요청 조건에 해당하지 않는 경우(PUT/DELETE 메서드, Authorization 헤더, application/json Content-Type 등), 브라우저는 실제 요청 전 Preflight 요청(사전 확인 요청)을 먼저 보낸다.

Preflight는 OPTIONS 메서드로 서버에 "이런 요청을 보내도 되냐?"고 물어보는 과정이다.

Preflight 요청 흐름
1
브라우저가 OPTIONS Preflight 전송
OPTIONS /api/data HTTP/1.1
Origin: https://app.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
2
서버가 허용 여부 응답
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
3
Preflight 통과 시 실제 요청 전송
POST /api/data HTTP/1.1
Origin: https://app.com
Content-Type: application/json
Authorization: Bearer eyJ...
4
서버 응답 (CORS 허용 헤더 포함)
⚡ Access-Control-Max-Age
Preflight 결과를 캐시하는 시간(초)이다. 86400이면 하루 동안 같은 Preflight를 재전송하지 않는다. 개발 중에는 낮게 설정하고, 프로덕션에서는 높게 설정하면 불필요한 OPTIONS 요청을 줄일 수 있다.

주요 CORS 응답 헤더

헤더설명예시
Access-Control-Allow-Origin허용할 출처. *은 모든 출처 허용 (자격증명 불가).https://app.com 또는 *
Access-Control-Allow-Methods허용할 HTTP 메서드 목록 (Preflight 응답에 포함).GET, POST, PUT, DELETE
Access-Control-Allow-Headers허용할 요청 헤더 목록 (Preflight 응답에 포함).Content-Type, Authorization
Access-Control-Allow-Credentials쿠키/인증 헤더 포함 요청 허용 여부. true* 불가.true
Access-Control-Expose-Headers브라우저 스크립트에 노출할 추가 응답 헤더.X-Custom-Header
Access-Control-Max-AgePreflight 결과 캐시 시간(초).86400

서버별 CORS 설정

# Express.js (Node.js)
const cors = require('cors');

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

// 특정 출처만 허용 (프로덕션 권장)
app.use(cors({
  origin: ['https://app.com', 'https://www.app.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
}));
# FastAPI (Python)
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.com"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
    max_age=86400,
)
# Nginx CORS 헤더 설정
location /api/ {
    add_header 'Access-Control-Allow-Origin' 'https://app.com' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type' always;

    # Preflight OPTIONS 처리
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Max-Age' 86400;
        return 204;
    }
}
🚫 흔한 실수
Access-Control-Allow-Origin: *credentials: true를 동시에 사용할 수 없다. 자격증명(쿠키, Authorization 헤더)이 포함된 요청에는 반드시 구체적인 출처를 명시해야 한다. 이를 무시하면 브라우저가 오류를 발생시킨다.

개발 환경에서 CORS 우회

# Vite 개발 서버 프록시 (vite.config.js)
# 브라우저 입장에서는 같은 출처로 보임
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

# Create React App (package.json)
{
  "proxy": "https://api.example.com"
}
✅ 핵심 요약
CORS는 SOP(동일 출처 정책)를 서버가 명시적으로 완화하는 메커니즘이다. 출처는 프로토콜+도메인+포트의 조합이며, 셋 중 하나만 달라도 Cross-Origin이다. 단순 요청은 바로 전송되지만, PUT/DELETE나 커스텀 헤더는 먼저 OPTIONS Preflight를 보낸다. 서버는 Access-Control-Allow-Origin 등의 응답 헤더로 허용 여부를 알리며, CORS는 브라우저만 강제하는 정책이다.
반응형

댓글