쿠키 때문에 삽질했던 경험 있으시죠?

쿠키 때문에 며칠씩 삽질했던 기억이 생생한데요. 쿠키 관련 함정과 해결책을 정리하고 두고두고 찾아와서 새로운 이슈가 생길 때마다 업데이트할 예정입니다.

가장 많이 하는 착각

// ❌ 착각
"domain=example.com으로 설정하면 example.com에서만 쿠키가 유효하다"

// ✅ 현실
"domain=example.com으로 설정하면 example.com과 모든 서브도메인에서 유효하다"

// 결과
Set-Cookie: sessionId=abc123; domain=example.com; path=/
✅ example.com
✅ www.example.com
✅ api.example.com
✅ admin.example.com

domain 속성을 생략하면?

// 응답 서버: api.example.com
Set-Cookie: sessionId=abc123; path=/

// 이 쿠키가 전송되는 도메인
✅ api.example.com (정확히 일치하는 도메인만)
❌ example.com        // 상위 도메인 설정 가능
❌ www.example.com    // 다른 도메인 설정 불가
❌ admin.example.com  // 하위 도메인 설정 불가

서브도메인에서 쿠키가 공유되는 조건

  • domain 속성 설정

  • domain 계층 준수

    응답 서버: api.example.com
    
    ✅ domain=example.com 
    ❌ domain=other.com
    ❌ domain=sub.api.example.com

path 설정 실수로 쿠키가 안 보내지는 케이스

// 응답 서버: api.example.com
Set-Cookie: sessionId=abc123; domain=example.com; path=/admin

// 요청 URL별 쿠키 전송 여부
✅ example.com/admin          → 쿠키 전송
✅ example.com/admin/users    → 쿠키 전송  
❌ example.com/               → 쿠키 전송 실패
❌ example.com/api            → 쿠키 전송 실패

일반적으로 / (모든 경로에서 쿠키 전송)

CORS 환경에서 쿠키가 안 보내질 때

CORS란?

Cross-Origin Resource Sharing의 줄임말로, 브라우저의 기본 보안 정책인 SOP(Same-Origin Policy)를 완화하여 다른 도메인 간에 리소스를 안전하게 공유할 수 있도록 하는 메커니즘

SOP란?

같은 출처(프로토콜 + 도메인 + 포트)에서만 리소스 접근 허용하는 브라우저 기본 보안 정책

웹에서는 credentials를 활성화하고 서버에서는 Access-Control-Allow-Origin에 도메인과 Access-Control-Allow-Credential을 활성화해야합니다.

체크포인트

  • 프론트엔드

    • credentials: “include” 설정(fetch 기준)

    • 개발자 도구 Network 탭에서 쿠키가 실제로 전송되는지

  • 백엔드

    • Access-Control-Allow-Origin에 구체적 도메인

    • Access-Control-Allow-Credentials: true 설정

    • 쿠키의 sameSite 속성(크로스도메인 여부에 따라)

fetch에서 쿠키가 안 보내져서 고생한 경험

// ✅ credentials 옵션 추가
fetch('<https://api.example.com/profile>', {
	credentials: 'include' // 쿠키를 포함해서 요청
})
  .then(res => res.json())
  .then(data => {
    // 401 에러
    console.log(data); // { error: "Unauthorized" }
  });

// ❌
fetch('<https://api.example.com/profile>')
  .then(res => res.json())
  .then(data => {
    // 401 에러
    console.log(data); // { error: "Unauthorized" }
  });

ajax client마다 포함시키는 방식의 차이

  • XMLHttpRequest

    • withCredentials: true

  • axios

    • withCredentials: true

  • fetch

    • credentials: include

와일드카드(*) 때문에 막혔던 이유

// express 기준

// ✅ 정상 동작
app.use(cors({
  origin: '*',           // 와일드카드 사용
  credentials: false     // 쿠키 포함 안 함
}));

// ✅ 정상 동작
app.use(cors({
  origin: '<https://example.com>', // 구체적인 도메인
  credentials: true
}));

// ❌
app.use(cors({
  origin: '*', // 🚨 와일드카드
  credentials: true
}));

Access to fetch at http://api.example.com/user from origin http://api.example.com has been blocked by CORS policy: 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'.

서버 설정 시 쿠키 옵션

  • domain: 어떤 도메인에서 쿠키를 받을지 결정(default는 해당 도메인)

  • path: 어떤 URL에서 쿠키를 받을지 결정(default는 모든경로(/))

  • secure: HTTPS에서만 쿠키 전송

  • httpOnly: JS로 쿠키 접근 차단

  • sameSite: 도메인기준으로 저장된 쿠키를 크로스사이트 요청에서 쿠키 전송 제어

    • strict: 같은 사이트에서만

    • lax: GET은 허용

    • none: 모든 크로스 사이트 요청 허용

태그 요청의 쿠키 자동 포함 문제

저희가 알고 있는 태그(img, script, link) 기반 요청은 웹의 호환성을 위해 CORS preflight 없이 실행되지만, fetch와 같은 외부 요청과 다르게 자동으로 포함되는 쿠키 때문에 CSRF 공격의 주요 경로가 됩니다.

CSRF를 방지하는 보편적인 방식은 2가지 정도가 있는데요.

  • 서버에서 CSRF 토큰 검증을 통해 실제 사용자 여부를 판단

  • SameSite 쿠키를 사용해서 크로스 사이트 쿠키 전송을 제한하는 방식

보안 관련 팁

  • XSS

    • CSP로 리소스 로딩 자체를 막으면 쿠키 전송이 될 수 없습니다.

      app.use((req, res, next) => {
        res.setHeader('Content-Security-Policy', 
          "script-src 'self' 'unsafe-inline' <https://cdn.jsdelivr.net>; " +
          "img-src 'self' data: https:; " +
          "style-src 'self' 'unsafe-inline' <https://fonts.googleapis.com>; " +
          "font-src 'self' <https://fonts.gstatic.com>; " +
          "connect-src 'self' <https://api.myapp.com>"
        );
        next();
      });
    • 쿠키 httpOnly 옵션을 활성화하여 javascript로 쿠키를 제어하지 못하도록 막습니다.

  • CSRF

    • 세션별 CSRF 토큰 생성 후 요청마다 검증

    • SameSite lax 또는 strict 설정

    • Origin/Referrer 값을 host 혹은 화이트리스트와 비교

배포 후 발견하는 쿠키 이슈

  • Expires vs Max-Age 헷갈리기

    // Expires는 구체적인 날짜/시간
    res.cookie('test', 'abc123', {
      expires: new Date('2024-12-31T23:59:59Z')  // 구체적인 날짜
    });
    
    // 🚨 쿠키가 즉시 만료
    res.cookie('test', 'abc123', {
      expires: 3600// 숫자를 바로 넣으면 Unix Timestamp 시작점(1970년 1월 1일) 기준
    });
    
    // ✅ 서버와 클라이언트 시간대가 다를 수 있기 때문에 상대 시간 사용
    res.cookie('test', 'abc123', {
      expires: new Date(Date.now() + 24 * 60 * 60 * 1000) 
    });
    
    // Set-Cookie: test=abc123; Expires=Wed, 13 May 2025 15:30:00 GMT
    // Max-Age는 상대 시간
    res.cookie('sessionId', 'abc123', {
      maxAge: 24 * 60 * 60 * 1000 // JS는 밀리초 -> HTTP 헤더는 초
    });
    // Set-Cookie: test=abc123; Max-Age=86400

마무리 체크리스트

개발 시 확인사항

배포 전 확인사항

참조

Last updated