Cross-Site Scripting · XSS
입력값이 브라우저에서 실행되는 문맥 이해
발생 상황
검색 결과, 게시글, 메모, 프로필 링크에서 외부 입력을 HTML로 출력하거나 DOM에 삽입할 때 발생합니다. 관리자만 보는 신고·문의 페이지도 공격 표면이 될 수 있습니다.
주요 유형
Reflected는 요청값이 응답에 즉시 반영되고, Stored는 저장된 값이 다른 방문 시 실행됩니다. DOM-based는 클라이언트의 source에서 sink로 값이 전달되는 흐름을 가리킵니다. 저장 여부와 실행 위치는 서로 다른 분류 축입니다.
분석할 때 주의할 점
문자열이 페이지에 보인다는 것과 코드가 실행된다는 것은 다릅니다. 응답 소스와 최종 DOM, HTML·속성·JavaScript·URL 문맥을 구분하세요. HttpOnly는 쿠키 읽기를 막지만 XSS로 발생하는 모든 행위를 차단하지는 않습니다.
대응 방안
일반 텍스트는 textContent와 프레임워크의 기본 이스케이프를 사용합니다. HTML을 허용해야 한다면 검증된 sanitizer로 정제합니다. URL은 허용 스킴을 검증하고 CSP를 추가 방어로 적용합니다. DOM 삽입 함수와 직접 HTML 출력 사용을 점검합니다.
자주 놓치는 부분과 특이사항
정렬값처럼 검색어 이외의 입력만 인코딩이 누락되기도 합니다. 여러 번의 디코딩이나 HTML 파서의 재해석으로 검증 시점과 실행 시점의 값이 달라질 수 있습니다. javascript: 링크는 저장뿐 아니라 실제 클릭 경로까지 확인해야 합니다.
XSS · Reflected, Stored, DOM 더 알아보기
Reflected XSS는 요청값이 응답에 반영되고, Stored XSS는 저장된 내용이 이후 방문자에게 렌더링됩니다. DOM XSS는 브라우저 코드가 URL 등의 입력을 위험한 DOM API로 전달하면서 발생합니다.
HTML 본문·따옴표로 둘러싸인 속성·JavaScript 문자열·URL은 각각 다른 문맥입니다. 검색어가 인코딩되어 있어도 정렬 파라미터나 preview 조각이 같은 처리를 받는다고 가정할 수 없습니다. 이벤트 핸들러와 javascript: 링크도 실행 지점이므로 문맥별 출력 인코딩과 프로토콜 검증이 필요합니다.
인코딩과 필터 우회 더 알아보기
URL 인코딩, HTML 엔티티, JSON 이스케이프, Base64는 서로 다른 변환입니다. 한 계층에서 검사한 값이 다음 계층에서 다시 디코딩되면 검사가 보지 못한 문자가 나타날 수 있습니다.
원본 요청 → 서버 파싱 → 응답 소스 → 브라우저 DOM을 차례로 비교합니다. 이중 인코딩은 무조건 가능한 우회가 아니라 실제 디코딩 횟수가 맞을 때만 의미가 있습니다. 브라우저 주소창과 Burp 편집기에서 같은 문자열이 같은 바이트로 전송되는지도 확인합니다.
요청과 구현 비교
반영에서 실행까지
입력 → HTTP 응답 → 브라우저의 DOM 구성 → 실행 지점 순으로 추적합니다. 무해한 표식이 응답에 포함된 것만으로 XSS는 아닙니다. Network의 원문과 Elements의 DOM을 비교하고, 격리된 브라우저에서 실제 실행을 별도로 확인해야 합니다.
// 위험: HTML로 해석
result.innerHTML = userInput;
// 일반 텍스트 출력
result.textContent = userInput;출력 문맥별 안전한 구현
일반 텍스트·HTML 속성·링크 URL·허용된 서식 HTML은 서로 다른 정책이 필요합니다. 텍스트는 텍스트 API를 사용합니다. 속성은 aria-label처럼 허용된 안전한 이름으로 제한하고, 이벤트 핸들러나 URL 속성은 별도 정책이 필요합니다. DOM API 자체가 임의의 속성을 안전하게 만들지는 않습니다. URL은 프로토콜을 검증하고, 서식 HTML은 검증된 정제 도구를 사용하세요.
title.textContent = userTitle;
// 속성명은 코드에서 고정하고 허용된 안전한 속성만 사용
button.setAttribute('aria-label', userLabel);
const target = new URL(userUrl, location.origin);
if (!['http:', 'https:'].includes(target.protocol)) throw new Error('허용되지 않은 주소');
link.href = target.href;
// HTML 서식이 꼭 필요한 경우에만 검증된 sanitizer 정책 적용적용 조건과 재검증
출력 문맥별 처리
일반 텍스트는 textContent 또는 기본 HTML 이스케이프를 사용하고, 링크는 http/https 같은 허용 스킴을 별도로 검사합니다. HTML을 허용하는 게시글은 검증된 sanitizer를 적용한 뒤 응답 원문과 최종 DOM을 비교하세요. 정제 전후에 정상 서식이 유지되는지도 확인합니다.
6번의 범위
6번 개인 메모는 작성자 자신의 저장과 재열람 경로를 검증합니다. 다른 사용자가 그 메모를 읽거나 관리자 권한으로 열람하는 조건은 이 문제의 목표에 포함되지 않습니다.