CORS 오류 해결 가이드: Access-Control-Allow-Origin 설정과 교차 도메인 차단 점검 체크리스트

이 글의 목적은 웹 브라우저에서 발생하는 CORS 오류를 신속하고 확실하게 진단하고 해결할 수 있도록, 원인 구조와 서버·클라이언트별 설정 방법, 재현 및 검증 절차, 보안상 주의사항을 체계적으로 정리하는 것이다.

1. CORS의 작동 원리 핵심 이해

CORS(Cross-Origin Resource Sharing)는 한 출처의 웹 페이지가 다른 출처의 리소스에 접근하려 할 때 브라우저가 적용하는 보안 정책이다. 출처는 프로토콜, 호스트, 포트의 조합으로 정의되며 하나라도 다르면 교차 출처로 간주한다. 서버가 명시적으로 허용 헤더를 반환하지 않으면 브라우저가 응답을 차단한다. 따라서 “서버가 허용하고 브라우저가 검증한다”는 관점에서 봐야 한다.

브라우저는 요청 유형에 따라 단순 요청과 프리플라이트 요청으로 나눈다. 단순 요청은 GET, HEAD, POST 중 제한된 헤더와 콘텐츠 유형을 만족할 때 바로 리소스를 요청한다. 그 외 대다수의 요청은 먼저 OPTIONS 메서드로 프리플라이트를 보내 서버가 허용하는지 확인한다. 서버가 적절한 CORS 응답 헤더를 반환하지 않거나, 프리플라이트를 차단하면 본 요청은 진행되지 않는다.

2. 자주 보이는 오류 메시지와 1차 조치

개발자 도구 콘솔에는 대개 “No ‘Access-Control-Allow-Origin’ header is present on the requested resource” 또는 “The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’”와 같은 오류가 표시된다. 다음 표는 메시지별 즉시 점검 항목이다.

오류 메시지 요약의미즉시 점검 항목
Access-Control-Allow-Origin 헤더가 없다 서버가 교차 출처를 허용하지 않는다 응답 헤더에 Access-Control-Allow-Origin 추가 여부 점검, 허용 도메인 정확히 지정 여부 확인
Credential 모드에서 * 사용 불가 쿠키·인증 포함 요청은 와일드카드를 허용하지 않는다 Access-Control-Allow-Origin을 구체적 Origin으로 반사하거나 화이트리스트 매칭으로 설정, Access-Control-Allow-Credentials:true 병행
프리플라이트 응답에 허용 메서드/헤더 없음 OPTIONS 응답이 불완전하다 Access-Control-Allow-Methods, Access-Control-Allow-Headers 목록 보강, 서버에서 OPTIONS 라우팅 허용
프리플라이트 4xx/5xx 프록시·WAF·로드밸런서에서 차단되었다 네트워크 경로별 로그 확인, 중간 장비에서 OPTIONS 허용 및 헤더 전파 설정
Vary 미설정으로 캐시 오염 다른 Origin의 캐시가 재활용된다 Vary: Origin 설정, CDN·프록시 캐시 키에 Origin 포함

3. 현장 점검 순서: 시크릿 모드·VPN 해제·캐시 삭제 후 재시도

클라이언트 환경 변수로 인해 오진되는 경우가 많다. 가장 빠른 배제 절차는 다음과 같다.

  1. 시크릿 모드에서 재현하여 확장 프로그램 간섭을 배제한다.
  2. VPN, 프록시, 보안 웹 게이트웨이를 일시 해제하고 동일 네트워크에서 재시도한다.
  3. 브라우저 캐시와 쿠키를 삭제하고 강력 새로고침을 수행한다.
  4. 다른 브라우저와 다른 OS에서 교차 검증한다.
  5. 개발자 도구 Network 탭에서 요청 Origin과 응답 헤더를 실제 값으로 확인한다.

이 과정에서 오류가 사라지면 환경 요인이었음을 의미하며, 유지보수 문서에 해당 조치를 표준화하여 사용자 가이드를 제공해야 한다.

4. 서버별 CORS 설정 요령

서버는 요청 Origin을 검증한 뒤 허용 시 특정 헤더를 추가해야 한다. 다음 표는 역할과 예시이다.

역할필수/선택 헤더설명
허용 출처 Access-Control-Allow-Origin: https://example.com 자격 증명 포함 시 와일드카드 사용 금지이다.
자격 증명 허용 Access-Control-Allow-Credentials: true 쿠키·Authorization 헤더·TLS 클라이언트 인증 사용 시 필요하다.
허용 메서드 Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS 프리플라이트 응답에서 표시해야 한다.
허용 헤더 Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With 클라이언트가 보낼 실제 커스텀 헤더를 반영해야 한다.
캐시 최적화 Access-Control-Max-Age: 3600 프리플라이트 결과를 브라우저가 캐싱한다.
캐시 분리 Vary: Origin 서버·CDN 캐시가 Origin별로 분기되도록 한다.

4.1 Nginx 설정 예시

location /api/ {
  if ($request_method = 'OPTIONS') {
    add_header 'Access-Control-Allow-Origin' $http_origin;
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With';
    add_header 'Access-Control-Max-Age' 3600;
    add_header 'Vary' 'Origin';
    return 204;
  }
  add_header 'Access-Control-Allow-Origin' $http_origin;
  add_header 'Access-Control-Allow-Credentials' 'true';
  add_header 'Vary' 'Origin';
  proxy_pass http://backend;
}

$http_origin을 그대로 반사하기 전에 허용 목록과 매칭하는 맵 또는 if 조건으로 화이트리스트 검증을 수행해야 한다.

4.2 Apache httpd 설정 예시

<IfModule mod_headers.c>
  SetEnvIf Origin "https://(www\.)?example\.com$" ORIGIN_OK=$0
  Header always set Access-Control-Allow-Origin "%{ORIGIN_OK}e" env=ORIGIN_OK
  Header always set Access-Control-Allow-Credentials "true" env=ORIGIN_OK
  Header always set Vary "Origin"
  Header always set Access-Control-Allow-Methods "GET,POST,PUT,DELETE,OPTIONS"
  Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
</IfModule>



Require all granted

 

mod_headers와 정규식으로 허용 출처를 제한하고, OPTIONS 메서드를 접근 허용해야 한다.

4.3 Node.js Express 예시

import express from 'express';
import cors from 'cors';

const allowlist = ['[https://example.com](https://example.com)', '[https://app.example.com](https://app.example.com)'];
const corsOptions = {
origin(origin, callback) {
if (!origin) return callback(null, false); // 필요시 로컬 개발에서 열지 여부 결정
const ok = allowlist.includes(origin);
callback(ok ? null : new Error('Not allowed by CORS'), ok);
},
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
methods: ['GET','POST','PUT','DELETE','OPTIONS'],
maxAge: 3600
};
const app = express();
app.use('/api', cors(corsOptions));
app.options('/api', cors(corsOptions)); 

에러 핸들러에서 프리플라이트 실패 시 적절한 상태 코드와 헤더를 함께 반환하도록 한다.

4.4 Spring Boot 예시

@Configuration
public class CorsConfig {
  @Bean
  public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
      @Override
      public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
          .allowedOrigins("https://example.com","https://app.example.com")
          .allowedMethods("GET","POST","PUT","DELETE","OPTIONS")
          .allowedHeaders("Content-Type","Authorization","X-Requested-With")
          .allowCredentials(true)
          .maxAge(3600);
      }
    };
  }
}

Spring Security를 함께 사용할 경우 Security 필터 체인에서도 CORS를 enable 해야 중복 차단을 피할 수 있다.

5. 자격 증명 모드와 와일드카드 금지 원칙

fetch나 XHR에서 credentials 옵션을 include 또는 same-origin으로 설정하면 브라우저는 쿠키·Authorization 헤더를 포함하거나 응답 쿠키를 저장하려 한다. 이때 서버는 Access-Control-Allow-Credentials:true와 함께 Access-Control-Allow-Origin에 정확한 출처를 넣어야 한다. *를 사용하면 브라우저가 보안을 이유로 차단한다. 또한 Set-Cookie를 사용하려면 SameSite=None; Secure 속성까지 일치해야 한다.

6. 프리플라이트 실패의 전형적 원인

  • 로드밸런서 또는 API 게이트웨이가 OPTIONS를 백엔드에 전달하지 않고 403으로 차단한다.
  • WAF가 Authorization 헤더나 커스텀 헤더를 위험으로 분류한다.
  • 서버 애플리케이션이 OPTIONS 라우트를 구현하지 않아 404를 반환한다.
  • CDN 캐시가 다른 Origin 응답을 재사용하여 Allow-Origin 헤더가 엉뚱한 값으로 섞인다.

이 경우 네트워크 경로를 hop별로 나눠서 접근 로그와 보안 로그를 확인해야 한다.

7. 재현과 진단을 위한 명령형 테스트

curl을 활용하면 브라우저 없이도 CORS 응답을 빠르게 확인할 수 있다.

# 단순 요청 모사
curl -i -H "Origin: https://example.com" https://api.example.net/data

# 프리플라이트 요청 모사

curl -i -X OPTIONS [https://api.example.net/data](https://api.example.net/data) 
-H "Origin: [https://example.com](https://example.com)" 
-H "Access-Control-Request-Method: PUT" 
-H "Access-Control-Request-Headers: Content-Type, Authorization" 

응답에 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Credentials, Vary가 올바르게 존재하는지 확인한다.

8. CDN·프록시·역프록시에서의 주의점

CDN 또는 리버스 프록시를 사용할 때는 다음 사항을 준수해야 한다.

  • Origin 헤더를 오리진 서버로 전달하고 응답의 Access-Control-* 헤더를 필터링하지 않도록 설정한다.
  • 캐시 키에 Origin을 포함하고 Vary: Origin을 응답에 추가한다.
  • OPTIONS 응답을 캐시할 때 Max-Age 범위를 관리하여 설정 변경이 즉시 반영되도록 무효화 워크플로를 갖춘다.

9. S3·Cloud Storage·CloudFront 환경

오브젝트 스토리지는 버킷 정책 또는 별도의 CORS 규칙을 설정해야 한다. 정적 파일을 서빙할 때도 교차 출처 요청이 발생할 수 있으므로, 필요한 메서드와 헤더만 최소 허용하는 원칙을 적용한다. CDN 도메인과 애플리케이션 도메인이 분리되어 있을 때 프리플라이트가 빈번하게 발생하므로 Max-Age를 활용하되 보안 정책 변경 가능성을 고려해 과도하게 길게 잡지 않는다.

10. 보안 고려: 너무 넓은 허용 금지

편의상 *를 남발하거나 모든 도메인을 반사 허용하면 CSRF 성격의 교차 도메인 호출이 가능해져 위험하다. 특히 Credential을 허용하는 API는 사내망·파트너망 등 제한된 도메인 화이트리스트만 허용해야 한다. 개발·스테이징·운영 도메인을 분리하고, 와일드카드 서브도메인 허용 시 DNS 하이재킹 가능성까지 고려한다.

11. SPA·프론트엔드에서의 선택지

로컬 개발에서 프록시를 두어 동일 출처처럼 보이게 만드는 방법이 있다. 예를 들어 Vite, CRA, Next.js의 개발 서버 프록시 기능을 활용하면 백엔드에 직접 요청하되 브라우저는 동일 출처로 인식한다. 운영 환경에서는 프런트와 API를 같은 도메인 하위 경로로 리버스 프록시하여 CORS 자체를 제거하는 것이 유지보수에 유리하다.

12. 요청 헤더와 본문이 단순 요청 조건을 벗어나는 경우

Content-Type이 application/json이면 단순 요청 조건을 충족하지 못하기 때문에 프리플라이트가 발생한다. 필요 최소한의 커스텀 헤더만 사용하고, 가능하면 simple 헤더 범위를 유지하여 프리플라이트 횟수를 줄이는 것이 성능상 유리하다.

13. 사례 기반 실전 트러블슈팅

사례 A: 로그인 API에서만 CORS 실패

원인 가설은 두 가지이다. 첫째, 로그인 응답에서 Set-Cookie에 SameSite=None; Secure가 누락되었다. 둘째, Access-Control-Allow-Origin이 *로 설정되었다. 해결은 크리덴셜 허용과 정확한 Origin 반환, Set-Cookie 속성 보완이다.

사례 B: PUT 요청만 403

프리플라이트에서 Access-Control-Allow-Methods에 PUT이 누락되었거나 WAF 규칙에서 차단되었다. WAF 로그를 점검하고 허용 목록에 메서드를 추가한다.

사례 C: 일부 사용자만 가끔 실패

CDN 캐시 키에 Origin이 없어서 다른 Origin의 응답이 재사용되었다. 응답에 Vary: Origin을 추가하고 캐시 키를 수정한다.

14. 체크리스트: 배포 전·장애 시 공통 절차

단계점검 항목합격 기준
로컬 재현시크릿 모드, VPN·프록시 해제, 캐시 삭제환경 영향이 배제된다
프리플라이트OPTIONS 라우트 허용, Allow-Methods/Headers 완비204 또는 200 응답과 올바른 헤더
Origin 허용화이트리스트 기반 동적 반사 또는 정적 설정정확한 Origin 값 반환
자격 증명Allow-Credentials:true, Origin 와일드카드 금지쿠키·토큰 동작 정상
캐시·CDNVary: Origin, 캐시 키 분리교차 오염 없음
로그웹서버·애플리케이션·WAF·프록시 로그프리플라이트와 본요청 모두 통과

15. HTTP 헤더 예시 묶음

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
Access-Control-Max-Age: 3600
Vary: Origin

16. 서버 사이드 렌더링과 쿠키 기반 세션

SSR 환경에서 API 호출을 서버 측에서 수행하면 브라우저의 CORS를 우회할 수 있다. 이때 서버와 API 간 통신은 백엔드 네트워크 내에서 이루어지므로 CORS가 적용되지 않는다. 다만 브라우저와 서버 간 쿠키 정책을 엄격히 구성해야 하며, CSRF 토큰 검증을 병행해야 한다.

17. CORS와 CSP를 혼동하지 말 것

CSP(Content-Security-Policy)는 브라우저가 로드할 수 있는 스크립트·이미지·프레임의 출처를 통제하는 정책이다. CORS는 네트워크 요청의 교차 출처를 제어한다. 두 정책은 목적이 다르며, 동시에 활성화되어 서로 다른 오류를 유발할 수 있다. 오류 메시지와 헤더를 구분하여 진단해야 한다.

18. 운영 환경 권장 정책

  • 허용 도메인은 환경변수나 설정 파일로 관리하고 배포 자동화에 포함한다.
  • 로그에 요청 Origin과 프리플라이트 결과를 기록하여 장애 분석 시간을 줄인다.
  • 테스트 자동화에 프리플라이트 시나리오를 포함하고, 변경 시 회귀 테스트를 수행한다.
  • CDN, WAF, 게이트웨이, 백엔드가 일관된 CORS 정책을 유지하도록 참조 구성을 문서화한다.

19. 로컬·스테이징·운영 분리 전략

로컬은 넓게 허용하되 비밀 데이터는 포함하지 않도록 분리한다. 스테이징은 운영과 동일한 CORS 설정을 사용하여 배포 전 검증을 보장한다. 운영은 최소 권한 원칙에 따라 엄격히 제한한다. 도메인 스키마 변경 또는 포트 변경 시 CORS 실패가 급증하므로 릴리즈 노트에 반드시 포함한다.

20. 요약: 가장 빠른 복구법

  1. 시크릿 모드에서 재현하고, VPN·프록시를 해제하고, 캐시를 삭제한다.
  2. 개발자 도구 Network에서 프리플라이트 응답과 본 요청 응답 헤더를 나란히 비교한다.
  3. 서버에서 Access-Control-Allow-Origin을 정확히 설정하고, 필요 시 Credentials와 Vary를 보강한다.
  4. CDN·WAF·프록시에서 OPTIONS와 헤더 전파를 허용한다.
  5. curl로 재현하여 브라우저 변수와 무관하게 원인을 분리한다.

FAQ

Access-Control-Allow-Origin: * 과 Credentials:true 를 함께 쓰면 왜 안 되나?

브라우저는 자격 증명 포함 요청에서 와일드카드 허용을 보안상 금지한다. 특정 Origin을 명시해야 안전한 신뢰 경계를 만들 수 있기 때문이다.

프리플라이트를 완전히 없앨 수 있나?

단순 요청 요건을 만족시키면 프리플라이트가 줄어든다. 그러나 인증 헤더나 JSON 콘텐츠 타입 등 현실 요구가 많아 완전 제거는 어렵다. 서버 캐싱과 Max-Age로 비용을 줄이는 접근이 현실적이다.

서버에서 모든 도메인을 반사 허용하면 편하지 않나?

편하지만 위험하다. 악성 사이트가 사용자의 브라우저를 통해 민감 API를 호출할 수 있다. 화이트리스트로 제한하는 것이 원칙이다.

SameSite=None; Secure 가 꼭 필요한가?

크로스 사이트 쿠키 전송이 필요하다면 반드시 필요하다. HTTPS가 아닌 환경에서는 Secure 속성 요구로 인해 쿠키가 전송되지 않는다.

CDN을 쓰면 왜 간헐적 CORS 오류가 생기나?

Origin별 캐시 분리가 안 되면 다른 Origin의 Allow-Origin 값이 섞인다. Vary: Origin과 캐시 키 분리로 해결한다.