소개
DOM 기반 XSS 치트 시트는 다음과 같습니다. 자주 사용하는 참조 를 사용하여 클라이언트 측 스크립트 인젝션에 대한 보호 기능을 찾고, 방지하고, 자동화할 수 있습니다. 본질적으로 사용자가 제어하는 입력( 출처)가 위험한 API( 싱크), 안전한 패턴으로 대체( 텍스트 콘텐츠, createElement또는 소독제)를 사용하고 빌드, 런타임 및 펜테스트 워크플로에 검사를 통합하세요. DOM XSS는 전적으로 브라우저에서 발생하고 기존의 많은 서버 측 필터를 우회하기 때문에 프론트엔드가 최후의 방어선이 되며, 대부분의 팀에서 여전히 자동화가 부족한 곳이기도 합니다.
DOM 기반 XSS란 무엇이며 왜 중요한가?
포트스위거에 따르면, DOM 기반 교차 사이트 스크립팅은 다음과 같은 경우에 발생합니다. "JavaScript는 URL과 같이 공격자가 제어할 수 있는 소스에서 데이터를 가져와 다음과 같이 동적 코드 실행을 지원하는 싱크에 전달합니다. eval() 또는 innerHTML." portswigger.net OWASP DOM 기반 XSS 방지 치트 시트는 저장/반영된 XSS와의 주요 차이점을 다음과 같이 강조합니다. 런타임 클라이언트 측 인젝션. cheatsheetseries.owasp.org+1
단일 페이지 앱(SPA), 타사 위젯의 과다 사용, 동적 DOM 구축 등 최신 애플리케이션에서는 페이로드가 서버 로그에 도달하지 않을 수 있고, 기존 WAF가 이를 놓칠 수 있으며, 개발자가 다음과 같은 사항을 충분히 고려하지 않는 경우가 많아 위험이 커집니다. 조각 식별자, 포스트 메시지 흐름또는 창.이름와 같은 일반적인 소스를 사용합니다. 이러한 변화를 인식하는 것이 보안 성숙도를 측정하는 첫 번째 단계입니다.
위협 매핑: 소스 → 싱크
보안 코딩은 다음과 같은 멘탈 맵에서 시작됩니다. 출처 (공격자 입력이 들어오는 곳) 및 싱크 (실행이 발생하는 위치). 한 보안 블로그에 요약되어 있습니다: "소스는 웹 페이지에서 사용자 입력을 추가할 수 있는 모든 위치입니다... 싱크는 소스에 삽입된 데이터가 이동하는 곳으로, 위생 처리되지 않으면 DOM 기반 XSS 취약점으로 이어질 수 있습니다." Medium
아래는 워크스테이션과 검토 체크리스트에 보관해야 할 간결한 표입니다:
| 소스(진입점) | 싱크(위험한 API) |
|---|---|
| 위치.해시, 위치.검색, URL 검색 매개변수 | innerHTML, insertAdjacentHTML, outerHTML |
| postMessage, 창.이름 | eval(), 새로운 함수(), setTimeout(문자열) |
| 문서 참조자, 로컬 저장소, 세션 저장소 | setAttribute('on...'), element.src = ... |
| 검증되지 않은 타사 위젯 데이터 | 모든 DOM 삽입 또는 암시적 코드 실행 |
OWASP가 강조하듯이 단일 기술로는 XSS를 방지할 수 없으며 적절한 싱크, 인코딩, 위생 처리 및 안전한 API를 결합해야 합니다. cheatsheetseries.owasp.org
코드 및 수정 사항이 포함된 실제 공격 패턴
예제 A - URL 조각을 통한 jQuery 선택기 삽입
취약한 스니펫:
js
$(window).on('hashchange', () => {{
const target = $(location.hash); // 사용자 제어
target[0].scrollIntoView();
});
공격자는 https://site/page.html#<img src="x" onerror="alert(1)"> - jQuery는 해시를 HTML/선택기로 취급하고 onerror 트리거.
수정:
js
const raw = 위치.해시.슬라이스(1);
const safeId = /^[A-Za-z0-9_-]+$/.test(raw) ? raw : null;
if (safeId) {
const target = document.getElementById(safeId);
if (target) target.scrollIntoView();
}
키: 유효성 검사, 해시를 HTML이 아닌 식별자로 취급합니다.
예시 B - postMessage → 평가 체인
취약한 스니펫:
js
window.addEventListener('message', e => {
eval(e.data); // 위험한
});
수정된 버전입니다:
window.addEventListener('message', event => {
if (event.origin !== '') 반환합니다;
시도 {
const msg = JSON.parse(event.data);
핸들메시지(msg);
} catch {
console.warn('잘못된 메시지 형식');
}
});
피하기 평가를 클릭하고 출처를 확인하고 안전한 구문 분석을 사용합니다.
예제 C - 마크다운 컨텍스트에서 편집기/미리보기 XSS
취약한:
js
preview.innerHTML = marked(userInput);
보안:
js
'dompurify'에서 DOMPurify를 가져옵니다;
const dirty = marked(userInput);
const clean = DOMPurify.sanitize(dirty);
preview.innerHTML = clean;
사용자 생성 HTML을 허용하는 경우 위생 처리가 필요합니다.
배포 가능한 구현: 운영 가능 상태로 만들기
안전한 DOM 도우미 - safe-dom.js
js
'dompurify'에서 DOMPurify를 가져옵니다;
export 함수 setSafeHTML(el, dirty) {
const clean = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b','i','a','p','ul','li','code','pre','img'],
ALLOWED_ATTR: ['href','src','alt','title','rel'],
FORBID_ATTR: ['onerror','onclick','style']
});
el.innerHTML = clean;
}
내보내기 함수 setText(el, text) {
el.textContent = 문자열(text ?? '');
}
export 함수 safeSetAttribute(el, name, val) {
if (/^on/i.test(name)) throw new Error('이벤트 핸들러 속성이 허용되지 않습니다');
el.setAttribute(이름, 문자열(값));
}
이 라이브러리를 사용하여 안전한 DOM 작업을 중앙 집중화하고 인적 오류를 줄이세요.
정적 적용 - ESLint 샘플
js
// .eslintrc.js
규칙: {
'제한되지 않은 구문': [
'오류',
{ selector: "AssignmentExpression[left.property.name='innerHTML']", 메시지: "safe-dom.setSafeHTML 또는 textContent를 사용하세요." },
{ selector: "CallExpression[callee.name='eval']", message: "Avoid eval()" },
{ selector: "CallExpression[callee.property.name='write']", message: "문서.write()를 피하세요." }
]
}
사전 커밋 후크와 결합(허스키, 보푸라기 단계)를 사용하여 위험한 패턴을 차단합니다.
CI/퍼펫티어 테스트 - GitHub 액션
.github/workflows/dom-xss.yml 를 누르면 퍼펫티어 테스트가 트리거됩니다:
// tests/puppeteer/dom-xss-test.js
js
const puppeteer = require('puppeteer');
(async ()=>{
const browser = await puppeteer.launch();
const page = await browser.newPage();
const logs = [];
page.on('console', msg => logs.push(msg.text()));
await page.goto(${URL}/page.html#<img src="x" onerror="console.log("xss")">);
await page.waitForTimeout(1000);
if (logs.some(l=>l.includes('XSS'))) process.exit(2);
await browser.close();
})();
탐지 기반 실패 빌드.
런타임 모니터링 - MutationObserver
js
(function(){
const obs = new MutationObserver(muts=>{{
muts.forEach(m=>{{
m.addedNodes.forEach(n=>{{
if(n.nodeType===1){
const html = n.innerHTML || '';
if(/on(error|click|load)|<script\\b/i.test(html)){
navigator.sendBeacon('/_monitoring/xss', JSON.stringify({
URL: location.href, 스니펫: html.slice(0,200)
}));
}
}
});
});
});
obs.observe(document.documentElement, {childList:true, subtree:true});
})();
예기치 않은 DOM 주입을 경고하는 스테이징에 유용합니다.
브라우저 보안 강화 - CSP 및 신뢰할 수 있는 유형
CSP 헤더:
pgsql
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-XYZ'; object-src 'none'; base-uri 'self';
신뢰할 수 있는 유형 스니펫:
js
window.trustedTypes?.createPolicy('safePolicy', {
createHTML: s => { throw new Error('직접 HTML 할당이 차단됨'); },
createScript: s => { throw new Error('직접 스크립트 생성 차단됨'); }
});
이렇게 하면 기본적으로 신뢰할 수 없는 싱크가 거부됩니다.
타사 스크립트 안전 - SRI
bash
openssl dgst -sha384 -binary vendor.js | openssl base64 -A
사용 <script src="vendor.js" integrity="sha384‑..." crossorigin="anonymous"></script> 를 클릭하여 고정하고 확인합니다.
자동화를 위한 Penligent와의 통합
팀에서 Penligent를 사용하는 경우, DOM XSS 보호를 지속적인 파이프라인으로 업그레이드할 수 있습니다. Penligent의 연구 문서에서는 다음과 같은 방법을 설명합니다. "런타임 오염 추적을 통한 DOM 기반 XSS 탐지... 서버 측 기술은 클라이언트 측 인젝션을 안정적으로 포착할 수 없습니다." 펜리전트
워크플로 예시:
- CI에서 규칙 집합으로 Penligent 스캔을 트리거합니다.
dom-xss와 같은 페이로드를 제공합니다.#<img src="x" onerror="alert(1)">. - Penligent는 헤드리스 플로우를 실행하고 PoC를 생성하며 웹훅을 통해 결과를 반환합니다.
- CI 분석 결과: 심각도가 높음 이상인 경우 빌드에 실패하고 페이로드 + 싱크 + 수정 권장 사항(예: 'replace
innerHTML와 함께safe-dom.setSafeHTML). - 개발자가 수정하고 CI를 다시 실행한 후 녹색일 때만 병합합니다.
이렇게 하면 참조(이 치트 시트) → 코드 정책 → 자동 감지 → 체계적인 수정으로 이어지는 루프가 닫힙니다.
결론
프런트엔드는 더 이상 "단순한 UI"가 아닙니다. 그것은 공격 표면. 이 치트 시트는 클라이언트 측 인젝션 이해, 위험한 싱크 교체, 안전한 헬퍼 라이브러리 구축, 정적/CI/런타임 탐지 배포, Penligent와 같은 플랫폼으로 자동화하는 방법을 안내합니다. 다음 단계: 코드베이스에서 금지된 싱크가 있는지 스캔하세요(innerHTML, 평가)를 채택하고 safe-dom 라이브러리, 린트 규칙 적용, 헤드리스 테스트와 실제 펜테스트 로직 통합, 프로덕션/스테이징 모니터링, 써드파티 리소스 고정 등의 기능을 제공합니다. DOM XSS를 보호하는 방법은 다음과 같습니다. 불가능 우연에 의존하는 것이 아닙니다.

