Cursor가 API에서 계속 와일드카드 CORS 헤더를 생성하는 이유
요약
Cursor와 같은 AI 에디터가 보안에 취약한 CORS 설정 코드를 생성하는 원인을 분석합니다. 학습 데이터에 포함된 'Origin 반사' 패턴이 개발 편의성을 위해 사용되면서 발생하는 보안 위협과 올바른 해결 방법을 제시합니다.
핵심 포인트
- AI 에디터는 학습 데이터의 편향성으로 인해 보안에 취약한 Origin 반사 코드를 생성함
- Origin 반사 방식은 인증된 세션 쿠키를 탈취할 수 있는 심각한 보안 허점을 만듦
- 와일드카드(*) 대신 명시적인 허용 목록(Allowlist)을 사용하는 것이 필수적임
- 로컬 개발 시에도 환경 변수를 통해 개발용과 프로덕션용 허용 목록을 분리 관리해야 함
요약 (TL;DR)
- AI 에디터들은 요청의 Origin 헤더를 그대로 반사(reflect)하면서 동시에 자격 증명(credentials)을 허용하는 CORS 미들웨어를 계속해서 생성합니다. 이로 인해 인터넷상의 어떤 사이트든 귀하의 API에 인증된 호출을 보내고 응답을 읽을 수 있게 됩니다.
- Origin을 반사하는 방식은 모든 개발 포트와 도구에서
req.headers.origin은 요청자가 제어할 수 있는 값입니다. evil-site.com에서 호스팅되는 페이지가 Origin: evil-site.com을 보내면, 서버는 이를 그대로 반사(echo)하고 브라우저는 일치하는 것으로 인식합니다. 자격 증명 (Credentials) 사용이 허용되어 있으므로, 피해자의 세션 쿠키 (Session Cookie)도 함께 전송됩니다. 이제 귀하의 API가 노출하는 모든 인증된 GET 또는 POST 요청은 피해자가 신뢰할 의도가 전혀 없었던 페이지에서도 읽을 수 있고 호출할 수 있게 됩니다.
왜 이런 일이 계속 발생하는가
Origin 반사 (Origin reflection) 방식이 학습 데이터에 남아있는 이유는, 이것이 로컬 개발 환경에서 CORS 에러를 사라지게 만드는 가장 빠른 해결책이며, 거의 모든 공개 코드 스니펫 (Snippet)이 그 방식에 최적화되어 있기 때문입니다. Stack Overflow의 답변, 보일러플레이트 (Boilerplate) 리포지토리, 그리고 "빠른 해결책"을 담은 Gist들은 모두 동일한 세 줄의 코드를 사용합니다. 왜냐하면 이 코드는 localhost:3000, localhost:5173, 또는 Vercel 프리뷰 URL 중 어디에서 테스트하든 동일하게 작동하기 때문입니다. 어떤 Origin이 실제로 허용되어야 하는지에 대해 아무도 고민할 필요가 없습니다.
명시적인 허용 목록 (Allowlist)을 작성하려면 프로덕션 프론트엔드의 도메인을 미리 알고 있어야 하는데, 이는 격리된 코드 스니펫에서는 알 수 없는 정보입니다. 따라서 학습 데이터는 어디서나 작동하고 아무것도 제한하지 않는 버전으로 편향됩니다. AI 에디터는 그 패턴을 충실히 재현하며, 개발자가 배포 전에 실제로 실행하는 모든 테스트를 통과하기 때문에 그대로 배포됩니다.
해결 방법
Origin 반사 대신, 각 들어오는 요청에 대해 명시적으로 확인되는 귀하가 실제로 신뢰하는 도메인 목록으로 교체하십시오.
// ✅ 명시적 허용 목록 (Allowlist), 반사 방식 아님
const allowedOrigins = [
'https://app.example.com',
...
동일한 해결책의 Python/Flask 버전:
from flask_cors import CORS
CORS(
...
로컬 개발을 위해서는 와일드카드나 "일단은 그냥" 식의 반사 방식을 사용하는 대신, localhost 포트들을 허용 목록에 명시적으로 추가하십시오. 환경 변수 (Environment variables)를 사용하는 것이 좋습니다. 개발용 허용 목록 하나와 프로덕션용으로 제한된 허용 목록 하나를 각각 명시적으로 관리하십시오.
FAQ
Q: Access-Control-Allow-Origin: *를 credentials와 함께 사용하는 것이 실제로 위험한가요?
A: 브라우저는 정확히 그 조합을 즉시 차단합니다. 그래서 실제 발생하는 버그는 Origin Reflection (오리진 반사) 형태입니다. 요청의 Origin 헤더를 그대로 응답에 반사하면서 credentials를 허용하면, 와일드카드가 달성하려 했던 것과 동일한 결과를 만들어내며 브라우저에 의해 차단되지도 않습니다.
Q: CORS 허용 목록(allowlist)이 인증(authentication)을 대체할 수 있나요?
A: 아니요. CORS는 어떤 브라우저 기반의 Origin이 응답을 읽을 수 있는지를 제어하는 것이지, 엔드포인트를 호출할 권한이 누구에게 있는지를 제어하는 것이 아닙니다. 여전히 실제 인증(auth) 체크가 필요합니다. CORS는 신뢰할 수 없는 페이지가 피해자의 세션을 사용하는 것을 브라우저 차원에서 막아주는 두 번째 계층입니다.
Q: 지금 당장 제 API에서 이 문제를 어떻게 확인할 수 있나요?
A: Access-Control-Allow-Origin 값이 고정된 목록 대신 요청 헤더에서 가져오는 CORS 미들웨어(middleware)가 있는지 확인하십시오. 만약 req.headers.origin 또는 req.get('origin')이 응답에 직접 작성되어 있다면, 그것이 바로 해당 패턴입니다.
저는 이를 위해 SafeWeave를 사용해 왔습니다. 이 도구는 MCP 서버로서 Cursor 및 Claude Code에 연결되어, 제가 다음 단계로 넘어가기 전에 이러한 패턴들을 찾아냅니다. semgrep 및 gitleaks를 사용한 기본적인 pre-commit hook만으로도 이 포스트에 언급된 내용의 대부분을 잡아낼 수 있습니다. 중요한 것은 어떤 도구를 사용하든 이를 조기에 발견하는 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기