CORS policy 에러로 알아보는 CORS, Origin 개념잡기
들어가며
웹 개발을 하다 보면 다음과 같은 에러를 콘솔에서 자주 마주치게 됩니다.
Access to fetch at 'https://api.example.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.
이 에러는 CORS(Cross-Origin Resource Sharing) 정책 때문에 발생합니다. 이 글에서는 CORS와 Origin의 개념을 이해하고, 실제 개발 상황에서 어떻게 해결할 수 있는지 알아보겠습니다.
Origin이란?
Origin의 정의
Origin(출처)은 웹 페이지의 프로토콜, 도메인, 포트를 조합한 고유 식별자로 Origin의 구성요소 중 하나라도 다르면 다른 Origin입니다.
Origin은 다음 세 가지 요소로 구성됩니다:
- 프로토콜(Protocol):
http://또는https:// - 도메인(Domain):
example.com - 포트(Port):
:80,:3000,:8080등
Origin 예시
// 같은 Origin
https://example.com/page1 → Origin: https://example.com
https://example.com/page2 → Origin: https://example.com
// 다른 Origin (프로토콜이 다름)
http://example.com → Origin: http://example.com
https://example.com → Origin: https://example.com
// 다른 Origin (도메인이 다름)
https://example.com → Origin: https://example.com
https://api.example.com → Origin: https://api.example.com
// 다른 Origin (포트가 다름)
https://example.com → Origin: https://example.com
https://example.com:3000 → Origin: https://example.com:3000
JavaScript에서 Origin 확인하기
// 현재 페이지의 Origin 확인
console.log(window.location.origin);
// 예시: https://example.com
// URL에서 Origin 추출
const url = new URL('https://example.com:3000/path?query=1');
console.log(url.origin);
// 예시: https://example.com:3000
Same-Origin Policy (동일 출처 정책)
Same-Origin Policy란?
Same-Origin Policy(동일 출처 정책)1는 브라우저의 보안 메커니즘으로, 한 Origin에서 로드된 스크립트가 다른 Origin의 리소스에 접근하는 것을 제한합니다.
Same-Origin Policy의 목적
- 쿠키 보호: 다른 사이트가 사용자의 쿠키를 읽거나 수정하는 것을 방지
- CSRF 공격 방지: 악의적인 사이트가 사용자 인증 정보를 이용해 요청을 보내는 것을 방지
- 데이터 유출 방지: 민감한 정보가 다른 Origin으로 전송되는 것을 차단
Same-Origin Policy가 적용되는 경우
// ❌ 차단됨: 다른 Origin으로의 ajax 요청
fetch('https://api.example.com/data')
.then(response => response.json())
.catch(error => console.error('CORS error:', error));
// ❌ 차단됨: 다른 Origin의 iframe 접근
const iframe = document.querySelector('iframe');
iframe.contentWindow.document; // SecurityError 발생
// ✅ 허용됨: 같은 Origin으로의 요청
fetch('/api/data')
.then(response => response.json());
CSRF (Cross-Site Request Forgery)란?
CSRF 공격이란?
CSRF(Cross-Site Request Forgery)2는 사용자가 의도하지 않은 요청을 악의적인 웹사이트가 대신 보내는 공격입니다. 사용자가 로그인한 상태에서 악의적인 사이트를 방문하면, 해당 사이트가 사용자의 인증 정보(쿠키 등)를 이용해 사용자 모르게 요청을 보낼 수 있습니다.
CSRF 공격 예시
<!-- 악의적인 사이트 (evil.com) -->
<img src="https://bank.com/transfer?to=attacker&amount=1000" />
<!-- 사용자가 이 이미지를 보는 순간,
로그인된 bank.com 세션으로 자동으로 송금 요청이 전송됨 -->
CORS와 CSRF의 관계
- CORS는 CSRF를 완전히 막지 못합니다: CORS는 브라우저의 정책이지만, 서버가 CORS를 허용하면 CSRF 공격이 여전히 가능할 수 있습니다.
- CORS + CSRF 토큰: CORS를 사용하더라도 CSRF 토큰을 함께 사용하여 보안을 강화해야 합니다.
CSRF 방어 방법
- CSRF 토큰 사용: 서버에서 생성한 토큰을 요청에 포함
- SameSite 쿠키: 쿠키에
SameSite=Strict속성 설정 - Referer 검증: 요청의 Referer 헤더 확인
CORS (Cross-Origin Resource Sharing)
CORS란?
CORS(Cross-Origin Resource Sharing)3는 Same-Origin Policy를 완화하여, 서버가 명시적으로 허용하는 경우 다른 Origin에서 리소스에 접근할 수 있도록 하는 메커니즘입니다.
CORS 동작 방식
CORS는 프리플라이트(Preflight) 요청과 실제 요청 두 단계로 이루어집니다.
1. Simple Request (단순 요청)
다음 조건을 모두 만족하면 프리플라이트 없이 바로 요청이 전송됩니다:
- 메서드:
GET,POST,HEAD만 허용 - 헤더: 특정 헤더만 허용 (
Content-Type은text/plain,multipart/form-data,application/x-www-form-urlencoded만) - 커스텀 헤더 없음
// Simple Request 예시
fetch('https://api.example.com/data', {
method: 'GET',
headers: {
'Content-Type': 'text/plain'
}
});
2. Preflight Request (프리플라이트 요청)
Simple Request 조건을 만족하지 않으면, 브라우저가 먼저 OPTIONS 메서드로 프리플라이트 요청을 보냅니다.
// Preflight가 필요한 요청
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({ data: 'value' })
});
프리플라이트 요청 흐름:
1. 브라우저 → 서버: OPTIONS 요청 (프리플라이트)
Headers:
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
2. 서버 → 브라우저: OPTIONS 응답
Headers:
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 86400
3. 브라우저 → 서버: 실제 POST 요청 (프리플라이트 성공 시)
CORS 헤더 설명
서버에서 설정하는 CORS 헤더
| 헤더 | 설명 | 예시 |
|---|---|---|
Access-Control-Allow-Origin |
허용할 Origin 지정 | * 또는 https://example.com |
Access-Control-Allow-Methods |
허용할 HTTP 메서드 | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers |
허용할 요청 헤더 | Content-Type, Authorization |
Access-Control-Allow-Credentials |
쿠키/인증 정보 포함 허용 | true |
Access-Control-Max-Age |
프리플라이트 캐시 시간(초) | 86400 (24시간) |
Access-Control-Expose-Headers |
클라이언트가 읽을 수 있는 응답 헤더 | X-Custom-Header |
클라이언트에서 보내는 CORS 헤더
| 헤더 | 설명 |
|---|---|
Origin |
요청을 보내는 Origin |
Access-Control-Request-Method |
실제 요청에서 사용할 메서드 (프리플라이트) |
Access-Control-Request-Headers |
실제 요청에서 사용할 헤더 (프리플라이트) |
실제 구현 예시
서버 측 구현 (Node.js/Express)
const express = require('express');
const app = express();
// CORS 미들웨어 설정
app.use((req, res, next) => {
// 허용할 Origin 목록
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
'http://localhost:3000' // 개발 환경
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// 인증 정보 포함 허용 (쿠키, Authorization 헤더 등)
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 프리플라이트 요청 처리
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '86400'); // 24시간 캐시
return res.sendStatus(200);
}
next();
});
// 또는 cors 라이브러리 사용
const cors = require('cors');
app.use(cors({
origin: ['https://myapp.com', 'http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
app.get('/api/data', (req, res) => {
res.json({ message: 'CORS가 설정된 응답입니다!' });
});
app.listen(3000);
클라이언트 측 구현 (JavaScript)
// 기본 fetch 요청
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include', // 쿠키 포함 (서버에서 Allow-Credentials 필요)
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('CORS Error:', error));
// Axios 사용 시
const axios = require('axios');
axios.get('https://api.example.com/data', {
withCredentials: true, // 쿠키 포함
headers: {
'Content-Type': 'application/json'
}
})
.then(response => console.log(response.data))
.catch(error => console.error('CORS Error:', error));
CORS 에러 해결 방법
1. 서버에서 CORS 헤더 설정 (권장)
가장 올바른 방법은 서버에서 CORS 헤더를 설정하는 것입니다.
// ❌ 잘못된 방법: 클라이언트에서 해결 불가능
// CORS는 브라우저 정책이므로 클라이언트 코드로 우회 불가
// ✅ 올바른 방법: 서버에서 CORS 헤더 설정
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
2. 개발 환경에서 프록시 사용
개발 중에는 프록시를 사용하여 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"
}
3. 서버리스 함수 사용
Netlify Functions, Vercel Functions 등을 사용하여 서버 측에서 API를 호출하면 CORS 문제를 피할 수 있습니다.
// Netlify Function 예시
exports.handler = async (event, context) => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
};
};
주의사항 및 보안 고려사항
1. Access-Control-Allow-Origin: * 사용 시 주의
// ⚠️ 위험: 모든 Origin 허용
res.setHeader('Access-Control-Allow-Origin', '*');
// ⚠️ 문제: credentials와 함께 사용 불가
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// → 브라우저가 에러 발생
// ✅ 안전: 특정 Origin만 허용
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
2. 민감한 정보 노출 방지
// ❌ 위험: 에러 메시지에 민감한 정보 포함
res.status(500).json({
error: 'Database password: xyz123'
});
// ✅ 안전: 일반적인 에러 메시지만 반환
res.status(500).json({
error: 'Internal server error'
});
3. CSRF 토큰 사용
CORS를 허용하더라도 CSRF 공격을 방지하기 위해 토큰을 사용하세요.
// 서버에서 CSRF 토큰 생성
const csrfToken = generateToken();
res.cookie('csrf-token', csrfToken, { httpOnly: false });
// 클라이언트에서 토큰 포함하여 요청
fetch('/api/data', {
method: 'POST',
headers: {
'X-CSRF-Token': getCookie('csrf-token')
}
});
실전 문제 해결 시나리오
시나리오 1: API 서버와 프론트엔드가 다른 도메인
문제:
- 프론트엔드:
https://myapp.com - API 서버:
https://api.myapp.com
해결:
// API 서버에서 설정
app.use(cors({
origin: 'https://myapp.com',
credentials: true
}));
시나리오 2: 개발 환경에서 localhost 사용
문제:
- 개발:
http://localhost:3000→http://localhost:8000 - 프로덕션:
https://myapp.com→https://api.myapp.com
해결:
const allowedOrigins = process.env.NODE_ENV === 'production'
? ['https://myapp.com']
: ['http://localhost:3000', 'http://localhost:3001'];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
시나리오 3: 쿠키가 전송되지 않음
문제: credentials: 'include'를 설정했는데도 쿠키가 전송되지 않음
해결:
// 클라이언트
fetch('https://api.example.com/data', {
credentials: 'include' // 쿠키 포함
});
// 서버
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// ⚠️ 주의: Allow-Origin을 '*'로 설정하면 credentials 작동 안 함
credentials: 'include'란?
credentials: 'include'4는 fetch API에서 사용하는 옵션으로, 쿠키, 인증 헤더, TLS 클라이언트 인증서 등 인증 정보를 요청에 포함하도록 설정합니다.
주요 특징:
- 쿠키 포함: Same-Origin과 Cross-Origin 요청 모두에서 쿠키를 자동으로 포함
- 인증 헤더 포함: Authorization 헤더 등 인증 관련 헤더를 포함
- 서버 설정 필요: 클라이언트에서
credentials: 'include'를 사용하려면 서버에서Access-Control-Allow-Credentials: true를 설정해야 함
다른 옵션들:
// credentials 옵션 값들
fetch(url, {
credentials: 'omit' // 인증 정보를 절대 포함하지 않음 (기본값)
});
fetch(url, {
credentials: 'same-origin' // 같은 Origin 요청에만 인증 정보 포함
});
fetch(url, {
credentials: 'include' // 모든 요청에 인증 정보 포함
});
주의사항:
credentials: 'include'를 사용할 때는 서버의Access-Control-Allow-Origin이*가 아닌 구체적인 Origin이어야 합니다- 보안상 필요한 경우에만 사용하고, 불필요한 경우
omit또는same-origin을 사용하는 것이 좋습니다
마무리
CORS와 Origin은 웹 보안의 핵심 개념입니다. 이 글에서 다룬 내용을 정리하면,
- Origin의 이해: 프로토콜, 도메인, 포트로 구성된 출처의 개념
- Same-Origin Policy: 브라우저의 기본 보안 메커니즘
- CORS의 동작: 프리플라이트 요청과 실제 요청의 흐름
- 실전 해결 방법: 서버 설정, 프록시, 서버리스 함수 활용
- 보안 고려사항: CSRF 방어와 credentials 옵션의 올바른 사용
올바르게 이해하고 구현하면 보안을 유지하면서도 필요한 경우 다른 Origin과 안전하게 통신할 수 있으며, CORS 에러를 빠르게 해결할 수 있습니다.
참고 자료:
-
MDN: Same-Origin Policy - 동일 출처 정책 설명 ↩
-
OWASP: CSRF - CSRF 공격에 대한 상세 정보 ↩
-
MDN: Fetch API credentials - credentials 옵션 상세 설명 ↩
댓글남기기