SameSite=None 쿠키 설정 (CSRF 위험)

SameSite=None Cookie

설명

민감한 쿠키에 SameSite=None을 설정하면, 브라우저의 다른 쿠키 정책이 허용하는 교차 사이트 요청에도 해당 쿠키가 전송될 수 있습니다. 별도의 CSRF 방어가 없으면 공격자가 사용자를 악성 페이지로 유도해 피해자 브라우저에서 인증 쿠키가 포함된 요청을 보내도록 만들 수 있습니다. 이로 인해 비밀번호 변경, 송금 등 상태 변경 요청이 사용자의 권한으로 실행될 수 있습니다.

잠재적 영향

  • CSRF 성공률 증가: 교차 사이트 요청에 인증/세션 쿠키가 포함되어, 공격자가 사용자의 권한으로 의도치 않은 요청을 실행시킬 수 있습니다.
  • 무단 상태 변경: 비밀번호 변경, 결제/송금, 프로필 수정 등 중요한 상태 변경이 사용자의 동의 없이 수행될 수 있습니다.
  • 계정 오남용 및 데이터 변조: 공격 대상 계정으로 스팸 발송, 설정 변경, 데이터 생성/수정/삭제가 가능해집니다.

해결 방법

  • 민감한 쿠키에는 SameSite=Strict를 사용하세요. (최소 Lax 권장)
  • 교차 사이트가 꼭 필요한 경우(3rd-party 임베드, SSO 등)에도 CSRF 방어를 반드시 추가하세요:
    • 서버에서 검증하는 세션 연계 CSRF 토큰이나 서명된 Double Submit Cookie 패턴을 사용하세요.
    • 상태 변경 요청의 Origin/Referer를 신뢰하는 정확한 출처와 비교하세요.
    • 상태 변경 요청은 GET이 아닌 POST/PUT/DELETE 등으로 제한하고, 서버에서 토큰/Origin을 검증하세요.
  • Express 등에서는 res.cookie(..., { sameSite: 'strict' | 'lax', secure: true, httpOnly: true })로 설정하세요.
  • SameSite=None을 부득이하게 사용할 때는 반드시 Secure 속성을 함께 사용하고, 상기 CSRF 방어를 병행하세요.

예시

변경 전

javascript
const crypto = require('crypto');
const express = require('express');
const app = express();

// 취약: 민감한 인증 쿠키에 SameSite=None만 설정, CSRF 방어 없음
app.post('/login', (req, res) => {
  const sessionId = crypto.randomBytes(32).toString('hex');
  res.cookie('auth', sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: 'none' // 교차 사이트 요청에도 쿠키 전송됨
  });
  res.send('logged in');
});

app.listen(3000);

변경 후

javascript
const express = require('express');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');

const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

// 보조 토큰 발급: 아래의 정확한 Origin/Referer 검증과 함께 사용
app.get('/form', (req, res) => {
  const token = crypto.randomBytes(16).toString('hex');
  const sessionId = crypto.randomBytes(32).toString('hex');
  res.cookie('csrfToken', token, { httpOnly: true, secure: true, sameSite: 'strict' });
  res.cookie('session', sessionId, { httpOnly: true, secure: true, sameSite: 'strict' });
  res.send(`<form method="POST" action="/transfer">
              <input type="hidden" name="csrf" value="${token}">
              <button type="submit">send</button>
            </form>`);
});

function verifyOrigin(req) {
  const value = req.headers.origin || req.headers.referer;
  if (typeof value !== 'string') return false;

  try {
    return new URL(value).origin === 'https://example.com';
  } catch {
    return false;
  }
}

// 안전: SameSite=Strict 사용 + Origin/Referer 검증 + CSRF 토큰 검증
app.post('/transfer', (req, res) => {
  if (!verifyOrigin(req)) return res.status(403).send('forbidden');
  const cookieToken = req.cookies.csrfToken;
  const formToken = req.body?.csrf;
  if (typeof cookieToken !== 'string' || !/^[0-9a-f]{32}$/.test(cookieToken) ||
      typeof formToken !== 'string' || formToken !== cookieToken) {
    return res.status(403).send('bad token');
  }
  res.send('ok');
});

app.listen(3000);

설명:

  • 변경 전: SameSite=None으로 설정된 인증 쿠키는 교차 사이트 요청에도 전송됩니다. CSRF 방어(토큰 검증, Origin/Referer 확인 등)가 없으므로 공격자는 피해자 브라우저로부터 인증이 포함된 요청을 임의로 발생시켜 상태 변경을 유도할 수 있습니다.
  • 변경 후: 예측 불가능한 세션 값을 발급하고 민감한 쿠키에 SameSite=Strict를 적용하여 교차 사이트 요청 시 쿠키가 전송되지 않도록 했습니다. Origin/Referer를 URL로 파싱해 정확한 출처를 비교하고, 비어 있지 않은 형식에 맞는 토큰이 일치하는지도 확인합니다. https://example.com.evil 같은 접두사는 허용하지 않습니다. 이 예시의 단순 쿠키 비교만을 단독 방어로 사용하지 마세요. 실제 로그인과 권한 검사는 생략했습니다.

참조