쿠키 스터핑과 CSRF

Heechan Kang·2025년 6월 22일
0
post-thumbnail

올해 초 쯤, 커뮤니티에서 '허니'라는 크롬 익스텐션이 논란이 되었던 적이 있었습니다.
잘 모르시는 분들을 위해 간략하게만 설명드리면, 이 허니라는 익스텐션은 사용자에게 특정 쇼핑몰 등의 할인 쿠폰을 자동으로 찾아서 적용해주는 서비스였는데요, 설명만 들으면 안쓰는 사람만 바보가 아닌가 싶을 정도로 좋은 도구죠.

그런데 여기서, 몇가지 문제점이 알려지게 됩니다.
자세한 내용이 궁금하신 분들은 관련 기사나 코딩애플(어둠의 생활코딩)님의 유튜브 영상도 많이 있으니, 한번쯤 찾아보시는것도 좋을 것 같습니다.

자, 서론은 여기까지 하고, 결국 이 익스텐션의 문제점을 다시 말해보자면 '사용자 몰래 쿠키를 조작해서 이익을 챙겼다'는 점입니다.
물론 정확히 추산 할 수는 없겠지만 원래 그 쿠폰, 추천인 등으로 이익을 받았어야 할 인플루언서들의 피해도 상당할 겁니다.

이런 사건을 그냥 쿠키를 사용한 단순한 어뷰징으로 치부하고 넘어갈 수도 있겠지만, 저는 이 사건을 보고 웹 공격 기법 중 하나인 CSRF(Cross-Site Request Forgery)가 떠오르더라구요. 물론 엄밀한 정의는 다르지만, "이런 쿠키 스터핑도 CSRF의 한 형태로 볼 수 있지 않을까?" 하는 질문에서부터 오늘의 글을 시작해보려 합니다.

쿠키 스터핑: 문제점은 무엇일까?

먼저 '쿠키 스터핑(Cookie Stuffing)'이 어떻게 동작하는지 간단히 짚고 넘어가겠습니다.

웹 브라우저와 쿠키에 대해 잘 모르시는분들을 위해 간단히 첨언하자면, 쿠키는 간단한 데이터를 클라이언트, 즉 사용자의 브라우저에 일시적으로 저장해두기 위해 사용되는 기술입니다.

이를 통해 사용자를 증명(로그인 관리) 하거나, 특정 상태값(다크모드)을 유지하거나, 특정 정보(이 사용자는 언제 방문했었음)를 저장해두는 등의 용도로 사용됩니다.

주요한 특징은 브라우저가 매 요청에 자동으로 쿠키를 첨부한다는 점입니다.
이 부분이 바로 오늘의 포인트이자 우리가 항상 주의하고, 해결해야할 문제점이기도 합니다.

정상적인 경우

자 그러면 이제, 정상적인 경우를 먼저 살펴보겠습니다.

[정상적인 경우]
1. 사용자가 인플루언서 등의 소개로 얻은 정보를 바탕으로 제품 구매 페이지로 이동합니다. 이 과정에서 사용자의 브라우저에는 추천인 쿠키가 심어집니다.
2. 사용자가 제품을 결제합니다. 이때 쿠키는 자동으로 서버에 전송됩니다.
3. 서버는 쿠키를 확인하고, 추천인에게 보상을 지급합니다.

이 경우에는 쿠키를 통해 '이 사용자는 누구의 추천을 통해 여기에 방문했다'라는 정보를 저장해두는 것이 목적이죠.
그래야 이를 통해 추천인에게 보상을 지급할 수 있기 때문입니다.

문제가 되는 경우

그러면 이제 문제가 되는 경우를 살펴보겠습니다.

[쿠키 스터핑 사례]
1. (위와 동일)사용자가 인플루언서 등의 소개로 얻은 정보를 바탕으로 제품 구매 페이지로 이동합니다. 이 과정에서 사용자의 브라우저에는 추천인 쿠키가 심어집니다.
2. 쿠키 추천 익스텐션이 쿠폰을 찾으면서 사용자 몰래 자신의 추천인 쿠키를 심습니다. 이 과정에서 사용자에게 제품을 소개해 준 인플루언서의 추천인 쿠키가 덮어씌워집니다.
3. (위와 동일)사용자가 제품을 결제합니다. 이때 쿠키는 자동으로 서버에 전송됩니다.
4. (위와 동일)서버는 쿠키를 확인하고, 추천인에게 보상을 지급합니다.

CSRF: 많이 들어는 봤는데..

그렇다면 CSRF(Cross-Site Request Forgery)는 무엇일까요?
CSRF의 핵심 구성요소는 다음과 같습니다.

[CSRF의 핵심 구성요소]
1. 사이트 간 요청(Cross-Site): 일반적으로 공격자는 실제 운영중인 서비스 내의 코드를 직접 조작할수 없습니다. 따라서 자신이 작성한, 혹은 자신이 조작할 수 있는 다른 사이트를 통해 피해 대상 사이트로 요청을 보내는 방식으로 공격합니다.
2. 사용자 의사와 무관한 요청(Request Forgery): 그중에서도 특히, 사용자가 의도하지 않은 요청을 공격자가 원하는 대로 전송하는 것을 의미합니다.
3. 이때 꼭 필요한 특성이 있는데, 이것이 바로 사용자의 브라우저가 자동으로 쿠키를 첨부하는 특성을 이용하는 것입니다.
4. (+@) 추가적으로, HTTP 요청은 모두 독립적이기에, 누구나 마치 '나'처럼 요청을 보낼 수 있다는 점도 중요합니다.

이제 이걸 시나리오로 좀 더 이해하기 쉽게 풀어보자면, 아래와 같습니다

CSRF 시나리오로 이해해보기

여기부터 CSRF가 실제로 어떻게 동작하는지 시나리오로 살펴보겠습니다.

일반적인 CSRF 공격 시나리오

[CSRF 공격 예시]
1. 사용자가 은행 사이트에 로그인합니다. 브라우저에 인증 쿠키가 저장되죠.
2. 사용자가 로그아웃하지 않고 다른 탭에서 공격자의 사이트를 방문합니다.
3. 공격자의 사이트에는 은행으로 송금 요청을 보내는 숨겨진 코드가 있습니다.
4. 사용자 모르게 브라우저가 은행으로 송금 요청을 보냅니다. 이때 인증 쿠키가 자동으로 포함됩니다.
5. 은행은 정상적인 요청으로 판단하고 송금을 처리합니다.

핵심은 "브라우저가 쿠키를 자동으로 보낸다"는 점입니다. 공격자는 이 특성을 악용해 사용자가 의도하지 않은 요청을 보내게 만들죠.

쿠키 스터핑과 CSRF의 공통점

사실 두개의 개념은 본질적으로는 꽤나 다릅니다.
하나는 단순히 얌체같은 쿠키 조작이고, 다른 하나는 정말 큰 위협이 될수도 있는 웹 공격 기법 중 하나니까요.
하지만 본질적으로는 둘 다 브라우저의 편의 기능을 악용해서 사용자를 기만한다는 점에서 공통점이 있습니다.

1. 사용자의 의도와 무관한 동작

  • CSRF: "아니, 나는 송금하려고 한 적이 없는데?"
  • 쿠키 스터핑: "어? 나는 추천인을 바꾸려고 한 적이 없는데?"

2. 브라우저의 자동 동작 악용

  • CSRF: 쿠키 자동 전송을 악용
  • 쿠키 스터핑: 쿠키 자동 저장/덮어쓰기를 악용

개발자로서 할 수 있는 일들

그래서 우리가 실제로 뭘 할 수 있을까요? 몇 가지 실용적인 방법들을 소개해 드리겠습니다.

1. 쿠키 설정 시 보안 옵션 활용하기

// 민감한 정보를 쿠키에 저장할때는 보안 옵션을 추가해주세요.
res.cookie('referral', referralId, {
    httpOnly: true,              // JavaScript로 접근 불가
    secure: isProduction,        // HTTPS에서만 전송
    sameSite: 'lax',             // 크로스 사이트 요청 제한
    maxAge: 24 * 60 * 60 * 1000, // 24시간 유효 시간
    path: '/',                   // 쿠키 적용 범위
    domain: 'example.com'        // 쿠키 적용 도메인
});

이 방법은 쿠키를 발급하는 시점에서 최대한 공격 표현을 줄이는 방법인데요.
자세한 내용은 아래와 같습니다.

1. httpOnly

httpOnly 옵션은 해당 쿠키를 JS로 접근 할 수 없도록 합니다. 따라서, 악의적인 스크립트 등으로 쿠키를 탈취하는 것을 방지할 수 있습니다.
무조건 켜면 좋은 옵션은 아니니, JS를 통한 클라이언트측 조작이 필요한가에 따라 결정해야 합니다.

일반적인 가이드를 한번 정리해보자면 아래와 같이 정리 할 수 있을 것 같네요.

  • 세션, 사용자 정보 등 민감 정보는 반드시 httpOnly: true
  • 다크모드, 언어설정 등 클라이언트단의 조작이 필요한 정보httpOnly: false

2. secure

이 옵션은 쿠키를 HTTPS 프로토콜에서만 전송할 수 있도록 합니다.
현대에 서비스하는 대부분의 서비스는 실질적으로 HTTPS가 강제되고 있기 때문에, 이 옵션은 꼭 켜두는게 좋습니다.

다만 개발환경에서는 몹시 귀찮기 때문에 비활성화 하는 경우가 많습니다.

const isProduction = process.env.NODE_ENV === 'production';

res.cookie('referral', referralId, {
    secure: isProduction,  // 프로덕션에서만 HTTPS 강제
    httpOnly: true
});

3. sameSite

이 옵션이 아마 가장 많이 헷갈리고, CSRF 공격을 방지하는데 가장 중요한 옵션이기도 합니다.

이 옵션은 기본적으로 lax로 설정되어 있는데, 이는 대부분의 경우 적절한 설정이기는 합니다.
흔히 해당 옵션을 켜면 크로스 사이트 요청, 그러니까 다른 도메인에서 오는 요청을 무조건 거절하는 것으로 오해하시는 경우가 많은데요. 이 옵션은 어디까지나 '사용자의 의도와 무관하게 발생하는 요청'에 대한 쿠키를 제한하는 옵션입니다.
다시말해, 크로스 사이트 요청인 경우에도 사용자가 의도적으로 발생시킨 요청이라면 쿠키를 전송하게 됩니다. 더 구체적이 예를 들자면 아래의 두 경우가 있습니다.

  • 사용자가 주소창에 직접 입력한 주소로 이동하는 경우
  • 사용자가 다른 도메인의 링크를 클릭하여 이동하는 경우

즉, 위에서 말했듯 '사용자가 의도하지 않은 백그라운드 리소스 및 JS 요청'에 대해서는 쿠키를 전송하지 않는 방식으로 동작하여, 사용성과 보안을 모두 최대한 보장하는 방식으로 동작합니다.

이외의 다른 값들은 아래와 같습니다.

  • strict: 같은 도메인에서만 쿠키를 보낼 수 있습니다.
  • none: 모든 도메인에서 쿠키를 보낼 수 있습니다.

여기서도 한번 가이드라인을 제공하자면, 이렇게 정리 할 수 있겠네요.

  • 세션, 사용자 정보 등 민감 정보는 반드시 sameSite: 'strict'
    • 피치못하게 필요한 경우에만 sameSite: 'lax'
  • 이외 대부분의 경우 sameSite: 'lax'
  • 광고나 사용자 추적, 추천등에 대해서는 sameSite: 'none'

4. maxAge

maxAge는 이름에서 알 수 있듯 쿠키의 유효 시간을 설정합니다.
이 옵션은 쿠키의 유효 시간을 설정함으로서 유출이나 악용 가능성이 있는 쿠키를 지정된 시간동안만 유지할 수 있게 합니다.
이또한 최대한 명시적으로 지정해 두는 것이 좋습니다.

5. path, domain

이 옵션은 쿠키의 적용 범위를 설정합니다.
기본적으로 쿠키는 현재 도메인에서만 적용되며, 이를 통해 쿠키의 범위를 제한할 수 있습니다.
하지만 필요에 따라 다른 도메인에서도 쿠키를 적용해야 하는 경우가 있습니다.
이때 이 옵션을 통해 쿠키의 적용 범위를 제한할 수 있습니다.

path의 경우 한 도메인 내에서 특정 경로에 대해서만 쿠키를 적용할 수 있고, domain의 경우 다른 도메인에서도 쿠키를 적용할 수 있습니다.
하지만 당연히 이때도 아무 도메인이나 허용할 수 있는것은 아니고, 자신 혹은 자신의 상위 도메인에 대해서만 허용할 수 있습니다.

  • path: 특정 경로(/, /admin, /api 등)에 대해서만 쿠키를 적용할 수 있습니다.
  • domain: 특정 도메인(example.com, .example.com, www.example.com 등)에 대해서만 쿠키를 적용할 수 있습니다.

2. CSRF 토큰 사용하기

앞서 계속 다루었듯, 결국 CSRF 공격이 이루어지는 이유는 브라우저가 쿠키를 자동으로 첨부하여 전송하기 때문입니다.
이를 정면으로 맞서는 방식이 바로 CSRF 토큰을 사용하는 방식이죠.
즉, 이제 개발자가 구현을 통해 '의도적으로' 토큰을 첨부함으로서 '이 요청은 사용자(개발자) 몰래 자동적으로 발생한 요청이 아니야.' 라고 증명하는 것이죠.

// 서버와 클라이언트가 모두 알고있는 토큰을 매번 '직접 첨부'하는 방식으로 사용합니다.
const csrfToken = "l23d4ktif..";

// 정상적인 사용자 요청
// → 쿠키(자동) + CSRF토큰(수동) 모두 포함. 정상 처리 ✅
fetch('/transfer', {
    method: 'POST',
    headers: {
        'X-CSRF-Token': csrfToken  // 개발자가 의도적으로 포함
    },
    body: JSON.stringify({ to: 'friend', amount: 100 })
});

// CSRF 공격 시도 (공격자 사이트에서)
// → 쿠키(자동) 포함, CSRF토큰(없음) → 서버에서 거부
fetch('https://bank.com/transfer', {
    method: 'POST',
    credentials: 'include',  // 쿠키는 자동으로 포함됨
    body: JSON.stringify({ to: 'attacker', amount: 1000000 })
    // 공격자는 Same-Origin Policy에 의해 CSRF 토큰 값을 직접 읽을 수 없음! ❌
});

그러면 이런 CSRF 토큰은 어떻게 생성할까요? 그리고 이 토큰은 결국 서버와 클라이언트가 모두 알고 있어야 하는데, 어떻게 관리해야하는 걸까요?

바로 위 사례에서 알 수 있듯, 만약에 CSRF 토큰이 고정된 값이라거나 '뻔한' 값이라면 여전히 공격자는 공격에 성공할 수 있습니다. 뭐 예를 들어 CSRF 토큰이 단순히 csrftoken123 이라면 그냥 이 값을 넣어서 요청을 보내면 되는 거니까요.

때문에 이를 방지하기 위해 CSRF 토큰은 랜덤하고 예측 불가능하게 생성되어야 합니다. 또한 주기적으로 새로운 값을 발급받아(Refresh) 사용해야 하죠.

이를 가장 편리하게 해결 할 수 있는 방식이 바로 Double Submit Cookie 방식입니다.

// 1. 서버: 토큰을 쿠키와 응답 모두에 포함
app.get('/api/csrf-token', (req, res) => {
    const token = crypto.randomBytes(32).toString('hex');
    
    res.cookie('csrf-token', token, {
        httpOnly: false,  // 클라이언트에서 읽을 수 있어야 함
        secure: true,
        sameSite: 'strict'
    });
    
    res.json({ csrfToken: token });
});

// 2. 클라이언트: 쿠키 값을 읽어서 헤더에 복사
function getCSRFToken() {
    const cookies = document.cookie.split(';');
    const csrfCookie = cookies.find(c => c.trim().startsWith('csrf-token='));
    return csrfCookie ? csrfCookie.split('=')[1] : null;
}

fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'X-CSRF-Token': getCSRFToken()  // 쿠키 값을 헤더에 복사
    },
    body: JSON.stringify({ to: 'friend', amount: 100 })
});

// 3. 서버: 쿠키 값과 헤더 값이 일치하는지 확인
app.post('/api/transfer', (req, res) => {
    const headerToken = req.headers['x-csrf-token'];
    const cookieToken = req.cookies['csrf-token'];
    
    if (!headerToken || headerToken !== cookieToken) {
        return res.status(403).json({ error: 'CSRF 검증 실패' });
    }
    
    // 검증 성공, 작업 수행
    performTransfer(req.body.to, req.body.amount);
});

이 방식은 CSRF 토큰을 특별히 발급하고 유지/관리하는 데에 큰 부담이 없다는 장점이 있습니다.
현대 브라우저의 구현 덕분에, 단순히 현재 요청에 쿠키와 CSRF 토큰을 모두 담고있음에도 해당 페이지의 개발자만이 토큰과 인증 쿠키를 동시에 사용 할 수 있기 때문입니다.

마치며

쿠키 스터핑 사건을 보면서 CSRF를 떠올린 건, 단순한 호기심에서 출발한 다소 뜬금없는 발단이기는 했습니다.
하지만 이를 통해 조금이나마 사용자와 개발자가 모두 더 경각심을 가지고, 어떻게 하면 더 사용자 친화적이면서도 보안상 안전한 구조를 만들 수 있을까 한번 더 생각해 볼 수 있었습니다.

사실 완벽한 보안은 없겠죠. 하지만 원리를 이해하고, 조금씩 계속 개선해나가는 자세가 중요하지 않을까요?

이 글이 조금이나마 도움이 되었으면 좋겠습니다.

profile
안녕하세요!

0개의 댓글