TIL - 20260601

juni·2026년 6월 1일

TIL

목록 보기
367/471

0601 풀스택 실무 기초 (5/N): CORS와 브라우저 보안 정책


✅ 1. CORS란 무엇인가?

  • CORS(Cross-Origin Resource Sharing)는 서로 다른 출처(Origin) 간의 HTTP 요청을 브라우저가 허용할지 결정하는 보안 정책입니다.
  • 쉽게 말하면, 프론트엔드 주소와 백엔드 API 주소가 다를 때 브라우저가 “이 요청을 허용해도 되는가?”를 검사하는 규칙입니다.
  • CORS는 서버가 아니라 브라우저에서 적용되는 보안 정책입니다.

➕ 1-1. Origin이란?

  • Origin은 다음 3가지가 합쳐진 값입니다.
프로토콜 + 도메인 + 포트
  • 아래 값들은 모두 서로 다른 Origin입니다.
https://example.com
http://example.com
https://api.example.com
https://example.com:3000
https://example.com:8080
항목값
프로토콜http, https
도메인example.com, api.example.com
포트3000, 8080, 443
  • 셋 중 하나라도 다르면 브라우저는 서로 다른 Origin으로 판단합니다.

✅ 2. CORS가 발생하는 상황

  • CORS는 프론트엔드와 백엔드가 다른 주소에서 동작할 때 자주 발생합니다.

➕ 2-1. 개발 환경 예시

React 개발 서버: http://localhost:5173
NestJS API 서버: http://localhost:3000
  • 둘 다 localhost지만 포트가 다릅니다.
  • 따라서 브라우저는 서로 다른 Origin으로 판단합니다.
http://localhost:5173 !== http://localhost:3000

➕ 2-2. 운영 환경 예시

프론트엔드: https://www.example.com
백엔드 API: https://api.example.com
  • 도메인이 다르기 때문에 서로 다른 Origin입니다.
  • 이 경우 백엔드 서버에서 https://www.example.com의 요청을 허용하도록 CORS 설정을 해야 합니다.

✅ 3. Same-Origin Policy

  • 브라우저에는 기본적으로 Same-Origin Policy라는 보안 정책이 있습니다.
  • 이 정책은 다른 Origin의 리소스에 마음대로 접근하지 못하게 막습니다.
  • 사용자의 쿠키, 인증 정보, 개인정보가 악성 사이트에 의해 탈취되는 것을 막기 위한 기본 보안 장치입니다.

➕ 3-1. Same-Origin Policy 예시

사용자가 bank.com에 로그인한 상태
  ↓
악성 사이트 evil.com 접속
  ↓
evil.com이 bank.com API에 몰래 요청
  ↓
브라우저가 보안 정책으로 차단
  • 이런 보안 정책이 없다면 악성 사이트가 사용자의 로그인 상태를 이용해 민감한 요청을 보낼 수 있습니다.
  • 그래서 브라우저는 기본적으로 다른 Origin 요청을 제한합니다.

✅ 4. CORS 에러 메시지 이해하기

  • CORS 문제가 발생하면 브라우저 콘솔에 다음과 비슷한 에러가 나타납니다.
Access to fetch at 'https://api.example.com/users'
from origin 'https://www.example.com'
has been blocked by CORS policy.
  • 이 메시지는 프론트엔드 코드가 무조건 잘못됐다는 뜻이 아닙니다.
  • 대부분은 백엔드 서버가 해당 Origin을 허용하지 않았기 때문에 발생합니다.

➕ 4-1. 실무에서 자주 착각하는 부분

  • CORS 에러는 서버가 아예 죽었다는 뜻이 아닙니다.
  • Postman에서는 정상 호출되는데 브라우저에서는 실패할 수 있습니다.
  • 이유는 CORS가 브라우저 보안 정책이기 때문입니다.
  • Postman, curl, 서버 간 통신에는 브라우저 CORS 정책이 적용되지 않습니다.
Postman 요청 성공
브라우저 요청 실패

→ 서버 API 자체는 살아 있지만, 브라우저에서 허용되지 않은 요청일 수 있음

✅ 5. CORS 관련 주요 응답 헤더

  • CORS는 서버가 응답 헤더를 통해 “어떤 Origin의 요청을 허용할지” 브라우저에게 알려주는 방식으로 동작합니다.

➕ 5-1. Access-Control-Allow-Origin

Access-Control-Allow-Origin: https://www.example.com
  • 특정 Origin의 요청을 허용합니다.
Access-Control-Allow-Origin: *
  • 모든 Origin을 허용합니다.
  • 단, 인증 쿠키나 Authorization 정보를 함께 보내는 요청에서는 *를 사용할 수 없습니다.

➕ 5-2. Access-Control-Allow-Methods

Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
  • 허용할 HTTP 메서드를 지정합니다.

➕ 5-3. Access-Control-Allow-Headers

Access-Control-Allow-Headers: Content-Type, Authorization
  • 클라이언트가 요청에 포함할 수 있는 헤더를 지정합니다.
  • JWT를 Authorization 헤더로 보낼 경우 반드시 허용해야 합니다.

➕ 5-4. Access-Control-Allow-Credentials

Access-Control-Allow-Credentials: true
  • 쿠키, 인증 헤더 같은 인증 정보를 포함한 요청을 허용할 때 사용합니다.
  • 프론트엔드에서 credentials: "include" 또는 axios의 withCredentials: true를 사용할 때 필요합니다.

✅ 6. Simple Request와 Preflight Request

  • 브라우저는 모든 요청을 바로 보내지 않습니다.
  • 일부 요청은 실제 요청 전에 서버가 허용하는지 먼저 확인합니다.
  • 이 사전 확인 요청을 Preflight Request라고 합니다.

➕ 6-1. Simple Request

  • 조건이 단순한 요청은 브라우저가 바로 서버에 보냅니다.

  • 예시:

    • GET
    • 일부 POST
    • 기본적인 Content-Type
GET /api/products

➕ 6-2. Preflight Request

  • 브라우저가 실제 요청 전에 OPTIONS 메서드로 서버에 확인 요청을 보냅니다.
OPTIONS /api/admin/users
Origin: https://www.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization
  • 서버가 이 요청에 대해 허용 헤더를 응답하면, 브라우저가 실제 요청을 보냅니다.
DELETE /api/admin/users/1
Authorization: Bearer access-token

➕ 6-3. Preflight가 자주 발생하는 경우

  • Authorization 헤더를 포함한 요청
  • Content-Type: application/json 요청
  • PUT, PATCH, DELETE 요청
  • 커스텀 헤더를 포함한 요청
  • 쿠키를 포함한 인증 요청

✅ 7. NestJS에서 CORS 설정하기

  • NestJS에서는 main.ts에서 CORS 설정을 적용할 수 있습니다.

➕ 7-1. 기본 CORS 허용

const app = await NestFactory.create(AppModule);

app.enableCors();

await app.listen(3000);
  • 개발 초기에는 편하지만, 운영 환경에서는 너무 넓은 허용이 될 수 있습니다.

➕ 7-2. 특정 Origin만 허용

const app = await NestFactory.create(AppModule);

app.enableCors({
  origin: ['https://www.example.com', 'https://admin.example.com'],
  methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
});

await app.listen(3000);
  • 운영 환경에서는 허용할 프론트엔드 도메인을 명확히 지정하는 것이 좋습니다.

➕ 7-3. 쿠키 인증을 사용하는 경우

const app = await NestFactory.create(AppModule);

app.enableCors({
  origin: ['https://www.example.com'],
  credentials: true,
});

await app.listen(3000);
  • 쿠키 기반 인증이나 HttpOnly Cookie를 사용할 경우 credentials: true가 필요합니다.
  • 이때 origin: '*'와 함께 사용할 수 없습니다.

✅ 8. 프론트엔드에서 credentials 설정하기

  • 쿠키 기반 인증을 사용할 때는 프론트엔드에서도 인증 정보를 포함해서 요청해야 합니다.

➕ 8-1. fetch 예시

await fetch('https://api.example.com/auth/me', {
  method: 'GET',
  credentials: 'include',
});

➕ 8-2. axios 예시

import axios from 'axios';

const api = axios.create({
  baseURL: 'https://api.example.com',
  withCredentials: true,
});
  • 서버에서 Access-Control-Allow-Credentials: true를 설정해도, 프론트엔드가 credentials 옵션을 넣지 않으면 쿠키가 함께 전송되지 않을 수 있습니다.

✅ 9. Authorization 헤더와 CORS

  • JWT를 Authorization 헤더에 담아 보내는 경우도 CORS 설정이 필요합니다.

➕ 9-1. 프론트엔드 요청 예시

await fetch('https://api.example.com/admin/users', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

➕ 9-2. 백엔드 CORS 설정

app.enableCors({
  origin: ['https://admin.example.com'],
  allowedHeaders: ['Content-Type', 'Authorization'],
});
  • Authorization 헤더가 허용되지 않으면 브라우저가 요청을 차단할 수 있습니다.

✅ 10. Nginx와 CORS

  • CORS는 보통 백엔드 애플리케이션에서 설정하지만, 상황에 따라 Nginx에서 처리하기도 합니다.
  • 특히 정적 파일, 프록시 서버, API 게이트웨이 구조에서는 Nginx 설정도 함께 확인해야 합니다.

➕ 10-1. Nginx CORS 예시

location /api/ {
  proxy_pass http://localhost:3000;

  add_header Access-Control-Allow-Origin "https://www.example.com";
  add_header Access-Control-Allow-Methods "GET, POST, PATCH, DELETE, OPTIONS";
  add_header Access-Control-Allow-Headers "Content-Type, Authorization";

  if ($request_method = OPTIONS) {
    return 204;
  }
}
  • 단, Nginx와 NestJS 양쪽에서 CORS 헤더를 중복으로 설정하면 문제가 생길 수 있습니다.
  • 가능하면 CORS 처리를 어디에서 할지 기준을 정하는 것이 좋습니다.

✅ 11. CORS와 쿠키 인증에서 자주 생기는 문제

➕ 11-1. origin에 *를 사용함

app.enableCors({
  origin: '*',
  credentials: true,
});
  • 이 설정은 쿠키 인증과 함께 사용할 수 없습니다.
  • credentials를 사용하는 경우 반드시 구체적인 Origin을 지정해야 합니다.

➕ 11-2. 프론트엔드에서 withCredentials 누락

axios.get('/auth/me');
  • 쿠키 인증을 쓰는데 withCredentials: true가 없으면 쿠키가 요청에 포함되지 않을 수 있습니다.
axios.get('/auth/me', {
  withCredentials: true,
});

➕ 11-3. SameSite 설정 문제

Set-Cookie: refreshToken=abc; HttpOnly; Secure; SameSite=Lax
  • 프론트엔드와 백엔드가 다른 사이트로 판단되는 구조에서는 SameSite 설정 때문에 쿠키가 전송되지 않을 수 있습니다.
  • 크로스 사이트 쿠키가 필요하면 보통 SameSite=None; Secure가 필요합니다.
Set-Cookie: refreshToken=abc; HttpOnly; Secure; SameSite=None
  • 단, SameSite=None은 HTTPS 환경에서 Secure와 함께 사용해야 합니다.

➕ 11-4. localhost와 127.0.0.1 혼용

프론트엔드: http://localhost:5173
백엔드: http://127.0.0.1:3000
  • localhost와 127.0.0.1은 개발자가 보기에는 비슷하지만 브라우저 입장에서는 다른 Origin으로 처리될 수 있습니다.
  • 개발 환경에서는 주소를 통일하는 것이 좋습니다.

✅ 12. CORS와 보안의 관계

  • CORS는 보안 기능이지만, 서버 API를 완전히 보호해주는 인증 기능은 아닙니다.
  • CORS는 “브라우저에서 다른 Origin 요청을 제한하는 정책”입니다.
  • API 자체의 인증과 권한 검사는 별도로 반드시 필요합니다.

➕ 12-1. CORS만 믿으면 안 되는 이유

  • Postman, curl, 서버 간 요청은 CORS 정책의 영향을 받지 않습니다.
  • 공격자는 브라우저가 아닌 다른 방식으로 API를 호출할 수 있습니다.
  • 따라서 중요한 API는 반드시 JWT, 세션, API Key, 권한 검사로 보호해야 합니다.
CORS: 브라우저 요청 제한
인증: 사용자가 누구인지 확인
인가: 해당 기능을 사용할 권한이 있는지 확인

✅ 13. CORS 문제 해결 순서

  • CORS 에러가 발생하면 무작정 origin: '*'로 열어버리면 안 됩니다.
  • 아래 순서대로 확인하는 것이 좋습니다.

➕ 13-1. 확인 순서

  1. 프론트엔드 Origin 확인
  2. 백엔드 API 주소 확인
  3. 브라우저 Network 탭에서 요청 URL 확인
  4. Preflight OPTIONS 요청이 실패했는지 확인
  5. 응답 헤더에 Access-Control-Allow-Origin이 있는지 확인
  6. Authorization 헤더를 사용하는지 확인
  7. 쿠키 인증이면 credentials 설정 확인
  8. Cookie의 SameSite, Secure, Domain 설정 확인
  9. Nginx 프록시가 헤더를 제거하거나 중복 추가하지 않는지 확인
  10. Postman이 아니라 브라우저 기준으로 재확인

✅ 14. 실무 체크리스트

➕ 14-1. 개발 환경 CORS 체크리스트

  1. React 개발 서버 포트와 백엔드 서버 포트를 확인했는가?
  2. localhost와 127.0.0.1을 혼용하지 않는가?
  3. 백엔드에서 개발 Origin을 허용했는가?
  4. Authorization 헤더를 allowedHeaders에 포함했는가?
  5. 쿠키 인증이면 credentials 옵션을 양쪽에 설정했는가?
  6. 브라우저 Network 탭에서 OPTIONS 요청을 확인했는가?

➕ 14-2. 운영 환경 CORS 체크리스트

  1. 운영 프론트엔드 도메인만 허용했는가?
  2. 관리자 페이지 도메인을 별도로 허용했는가?
  3. origin: '*'를 사용하지 않는가?
  4. 쿠키 인증에서 credentials: true와 구체적인 Origin을 사용했는가?
  5. HTTPS 환경에서 Secure Cookie를 사용하는가?
  6. Nginx와 백엔드 CORS 설정이 중복되지 않는가?
  7. 배포 후 실제 브라우저에서 로그인과 API 호출을 테스트했는가?

✅ 15. AI를 활용해 CORS 문제를 해결할 때 질문법

  • CORS 문제는 에러 메시지와 현재 구조를 정확히 알려줘야 해결이 빠릅니다.
  • 단순히 “CORS 에러 나요”라고 하면 원인 파악이 어렵습니다.

➕ 15-1. 좋은 질문 예시

React + NestJS 프로젝트에서 CORS 에러가 발생해.

프론트엔드 주소: http://localhost:5173
백엔드 주소: http://localhost:3000
인증 방식: JWT를 Authorization 헤더로 전송
요청 코드:
fetch('/api/admin/users', {
  headers: {
    Authorization: `Bearer ${token}`
  }
})

NestJS CORS 설정:
app.enableCors({
  origin: ['http://localhost:5173'],
  allowedHeaders: ['Content-Type']
})

브라우저 에러:
Request header field authorization is not allowed by Access-Control-Allow-Headers

어디를 수정해야 하는지 설명해줘.

➕ 15-2. AI 답변 검증 기준

  1. origin: '*'로 무조건 열라고 하지 않는가?
  2. Authorization 헤더 허용 여부를 확인하는가?
  3. 쿠키 인증과 JWT 헤더 인증을 구분하는가?
  4. Preflight OPTIONS 요청을 설명하는가?
  5. 운영 환경에서 허용 Origin을 제한하라고 안내하는가?
  6. CORS와 인증/인가를 혼동하지 않는가?

📌 요약

  • CORS는 서로 다른 Origin 간 요청을 브라우저가 허용할지 판단하는 보안 정책입니다.
  • Origin은 프로토콜, 도메인, 포트의 조합이며, 하나라도 다르면 다른 Origin으로 판단됩니다.
  • CORS는 브라우저에서 적용되는 정책이므로 Postman이나 서버 간 통신에서는 같은 문제가 발생하지 않을 수 있습니다.
  • 서버는 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Credentials 같은 헤더로 허용 범위를 알려줍니다.
  • Authorization 헤더, JSON 요청, PATCH, DELETE, 쿠키 인증 요청 등은 Preflight 요청이 발생할 수 있습니다.
  • NestJS에서는 app.enableCors()로 CORS를 설정할 수 있고, 운영 환경에서는 허용 Origin을 명확히 제한해야 합니다.
  • 쿠키 인증을 사용할 경우 서버의 credentials: true, 프론트엔드의 withCredentials: true 또는 credentials: "include" 설정이 함께 필요합니다.
  • CORS는 인증 기능이 아니므로, 중요한 API는 반드시 JWT, 세션, Role Guard 같은 인증/인가 로직으로 보호해야 합니다.

0개의 댓글