CSRF와 CORS: 브라우저 요청을 올바르게 보호하는 방법
깔끔한 API를 갖춘 웹 앱을 만들었는데, 로그에서 이상한 요청을 발견하거나 더 심하게는 보안 스캐너가 사이트의 CSRF 및 CORS 설정 오류를 지적하는 경우가 있습니다. 이 두 약어는 종종 함께 언급되지만, 서로 다른 문제를 해결합니다. 이를 잘못 이해하면 사용자가 취약해지거나 정당한 교차 출처 요청이 중단될 수 있습니다.
이 글에서는 CSRF와 CORS가 실제로 무엇인지, 브라우저 보안과 어떻게 상호작용하는지 명확히 설명하고, 기능을 손상시키지 않으면서 애플리케이션을 보호하는 구체적인 방법을 제시합니다.
CSRF란 무엇이며 왜 신경 써야 할까?
교차 사이트 요청 위조(CSRF)는 사용자의 브라우저가 인증된 사이트로 요청을 보내도록 속이는 공격입니다. 여러분이 bank.com에 로그인되어 있다고 가정해 봅시다. 그런 다음 <img src="https://bank.com/transfer?to=attacker&amount=1000">와 같은 이미지 태그가 포함된 악성 사이트를 방문합니다. 브라우저는 자동으로 은행 쿠키를 요청에 포함시키고, 은행에 CSRF 보호 기능이 없으면 이체가 실행됩니다.
핵심은 CSRF가 사이트가 사용자 브라우저를 신뢰하는 점을 악용한다는 것입니다. 공격자는 세션을 훔칠 필요 없이, 단지 브라우저가 사용자를 대신해 요청을 보내도록 하면 됩니다.
CSRF 공격의 작동 방식
CSRF 공격이 성공하려면 세 가지 조건이 충족되어야 합니다:
- 피해자가 인증되어 있어야 합니다(예: 유효한 세션 쿠키 보유).
- 공격자가 요청 구조(엔드포인트, 매개변수)를 알아야 합니다.
- 요청에 공격자가 추측할 수 없는 예측 불가능한 매개변수가 포함되지 않아야 합니다.
일반적인 대상은 상태를 변경하는 작업입니다: 이메일 변경, 자금 이체, 콘텐츠 게시, 권한 수정 등입니다.
CORS란 무엇이며 왜 존재할까?
교차 출처 리소스 공유(CORS)는 웹 페이지가 페이지를 제공한 도메인과 다른 도메인으로 요청을 보내는 것을 허용하거나 거부하는 브라우저 메커니즘입니다. 이는 동일 출처 정책(SOP)의 확장으로, 한 출처에서 로드된 문서나 스크립트가 다른 출처의 리소스와 상호작용하는 방식을 제한합니다.
CORS가 없으면 악성 사이트가 JavaScript를 사용해 로그인된 상태에서 은행 API의 데이터를 읽을 수 있습니다. CORS는 서버가 특정 교차 출처 요청을 명시적으로 허용할 수 있는 방법을 제공합니다.
동일 출처 정책(SOP)
두 URL은 프로토콜, 호스트, 포트가 동일하면 같은 출처입니다. 예를 들어 https://example.com/app과 https://example.com/api는 같은 출처를 공유하지만, http://example.com(다른 프로토콜)과 https://api.example.com(다른 호스트)은 그렇지 않습니다.
SOP는 한 출처의 스크립트가 다른 출처의 응답을 읽는 것을 방지합니다. CORS는 브라우저에 요청 허용 여부를 알려주는 HTTP 헤더를 추가하여 이를 완화합니다.
CSRF vs CORS: 주요 차이점
CSRF와 CORS가 서로 반대 개념이 아니라는 점을 이해하는 것이 중요합니다. 이들은 서로 다른 보안 문제를 다룹니다. CSRF는 권한 없는 상태 변경 요청을 방지하는 것이고, CORS는 어떤 출처가 응답을 읽을 수 있는지 제어하는 것입니다.
| 측면 | CSRF | CORS |
|---|---|---|
| 주요 목표 | 위조된 요청 방지 | 교차 출처 읽기 제어 |
| 공격 벡터 | 악성 사이트가 요청 트리거 | 악성 사이트가 응답 읽기 |
| 방어 메커니즘 | 토큰, SameSite 쿠키 | HTTP 헤더(Access-Control-*) |
| 브라우저 적용 | 없음(서버가 검증해야 함) | 있음(브라우저가 읽기 차단) |
CSRF 방어 방법
CSRF를 완화하는 여러 검증된 전략이 있습니다. 모두 필요하지는 않지만, 방어를 계층화하는 것이 현명합니다.
1. 안티 CSRF 토큰 사용
가장 강력한 방어는 각 상태 변경 요청에 고유하고 예측 불가능한 토큰을 포함하는 것입니다. 서버는 처리 전에 토큰을 검증합니다. 토큰은 사용자 세션에 연결되어야 하며 URL에 노출되지 않아야 합니다(리퍼러 헤더를 통한 유출 방지).
// 예: Node.js/Express에서 CSRF 토큰 생성 및 검증
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/process', csrfProtection, (req, res) => {
// 토큰이 자동으로 검증됨
res.send('OK');
});
2. SameSite 쿠키 설정
최신 브라우저는 쿠키에 SameSite 속성을 지원합니다. 이를 Lax 또는 Strict로 설정하면 브라우저가 교차 사이트 요청에 쿠키를 보내지 않도록 하여 많은 CSRF 공격을 차단합니다. Lax는 최상위 탐색(예: 링크 클릭)에서 쿠키를 허용하고, Strict는 모든 교차 사이트 요청을 차단합니다.
Set-Cookie: sessionId=abc123; SameSite=Lax; Secure; HttpOnly
3. Origin 및 Referer 헤더 확인
들어오는 요청의 Origin 또는 Referer 헤더를 확인합니다. 예상 도메인과 일치하지 않으면 요청을 거부합니다. 이는 간단하지만 효과적인 추가 계층입니다.
4. AJAX에 사용자 정의 헤더 사용
API가 JavaScript를 통해 사용되는 경우 X-Requested-With와 같은 사용자 정의 헤더를 요구합니다. 브라우저는 사용자 정의 헤더에 대해 CORS 프리플라이트를 적용하므로, 악성 사이트의 단순 폼 POST에는 이 헤더가 포함되지 않습니다.
CORS 올바르게 구성하기
CORS 설정 오류도 보안 문제를 일으킬 수 있습니다. 가장 흔한 실수는 자격 증명을 허용하면서 Access-Control-Allow-Origin: *를 설정하는 것입니다. 이 조합은 브라우저에서 금지되지만, 개발자들은 때때로 안전하지 않은 방식으로 우회하려고 합니다.
CORS 헤더 모범 사례
- 자격 증명이 관련된 경우 와일드카드 대신 정확한 출처를 지정합니다.
- 허용 메서드를 API가 지원하는 것으로 제한합니다(예:
GET, POST). - 허용 헤더를 앱이 실제로 사용하는 것으로 제한합니다.
Access-Control-Max-Age를 설정하여 프리플라이트 응답을 캐시하고 오버헤드를 줄입니다.Origin헤더를 무조건 반영하지 말고 화이트리스트에 대해 검증합니다.
# 예: CORS를 위한 Nginx 구성
location /api/ {
if ($http_origin ~* (https://(app|admin)\.example\.com)) {
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
}
if ($request_method = 'OPTIONS') {
return 204;
}
}
CORS는 서버가 아닌 브라우저에 의해 적용된다는 점을 기억하세요. 이는 인증이나 권한 부여를 대체하는 것이 아니라, 동일 출처 정책을 안전하게 완화하는 방법입니다.
모든 것을 종합하기
브라우저 요청 보안에는 계층적 접근이 필요합니다. 다음은 빠른 체크리스트입니다:
- 모든 상태 변경 작업에 안티 CSRF 토큰 사용.
- SameSite 쿠키를
Lax또는Strict로 설정. - 서버에서 Origin/Referer 헤더 검증.
- CORS를 정확하게 구성: 출처 화이트리스트, 메서드 제한, 자격 증명과 와일드카드 회피.
- 정기적으로 테스트: 보안 스캐너와 수동 검사로 앱 점검.
CSRF와 CORS의 서로 다른 역할을 이해하면 일반적인 함정을 피하고 더 안전한 웹 애플리케이션을 구축할 수 있습니다.
FAQ
CORS가 CSRF 공격을 방지할 수 있나요?
아니요, CORS는 CSRF를 방지하지 않습니다. CSRF 공격은 응답을 읽는 데 의존하지 않고 단순히 요청을 트리거합니다. CORS는 어떤 출처가 응답을 읽을 수 있는지 제어하지만, 요청이 전송되는 것을 차단하지는 않습니다. CSRF를 방지하려면 토큰, SameSite 쿠키 또는 출처 검증이 필요합니다.
Access-Control-Allow-Origin을 '*'로 설정하는 것이 안전한가요?
API가 자격 증명(쿠키, HTTP 인증)을 사용하지 않는 경우에만 '*'로 설정하는 것이 안전합니다. 자격 증명이 관련된 경우 브라우저는 응답을 거부합니다. 인증된 API의 경우 항상 정확한 출처를 지정하세요.
Authorization 헤더에 JWT를 사용하면 CSRF 보호가 필요한가요?
JWT를 쿠키에 저장하는 경우 쿠키가 자동으로 전송되므로 여전히 CSRF 보호가 필요합니다. JWT를 메모리에 저장하고 Authorization 헤더를 통해 전송하는 경우 공격자가 교차 출처에서 사용자 정의 헤더를 설정할 수 없으므로 CSRF는 문제가 되지 않습니다. 그러나 토큰을 안전하게 유지하려면 XSS로부터 보호해야 합니다.
API의 CORS 헤더를 테스트할 준비가 되셨나요? Nginx Log Analyzer를 사용하여 요청 패턴을 검사하고 의심스러운 교차 출처 시도를 발견하세요.