Studying Web Hacking (XSS)

XSS

XSS란?

XSS(Cross-Site Scripting)

  • 공격자가 웹페이지에서 자신이 만든 스크립트나 실행 가능한 브라우저 동작을 끼워 넣어, 그 페이지를 보는 사용자의 브라우저에서 실행되게 만드는 취약점
  • “서버가 털린다” 보단 사용자 세션/권한/화면/행동이 탈취 또는 조작된다

위험한 이유

  • 로그인된 사용자의 권한으로 요청을 보낼 수 있음
  • 페이지 내용을 바꿔 피싱 UI를 띄울 수 있음
  • 민감한 화면 정보, 토큰, 사용자 입력을 가로챌 수 있음
  • 관리자 페이지에 발생하면 피해가 훨씬 큼

성립 조건

3단계가 연결되면 생김

  • 공격자가 제어 가능한 입력값이 들어옴
  • 그 값이 적절히 인코딩/검증/정제되지 않는다
  • 브라우저가 그 값을 “문자열”이 아니라 “HTML/속성/스크립트”로 해석하는 위치에 넣는다

→ XSS는 “사용자 입력이 위험한 컨텍스트로 들어가는 문제”


컨텍스트란

  1. HTML 본문 컨텍스트

     <div>USER_INPUT</div>
    
  2. HTML 속성 컨텍스트

     <input value="USER_INPUT">
    
  3. URL 컨텍스트

     <a href="USER_INPUT">link</a>
    
  4. JavaScript 컨텍스트

     <script>
     	const data = "USER_INPUT";
     </sciprt>
    
  5. DOM sink 컨텍스트

     element.innerHTML = userInput;
    

대표적 종류

1. Reflected XSS

사용자 요청에 들어간 값이 바로 응답 HTML에 반사됨

예) 검색어, 에러 메시지, URL 파라미터

## 취약 예시 ##
<p>검색어: USER_INPUT<p/>

서버가 USER_INPUT을 그대로 넣으면 Reflected XSS

2. Stored XSS

공격 문자열이 DB 등에 저장됐다가 나중에 다른 사용자에게 보여질 때 실행

예) 게시글, 댓글, 닉네임, 관리자 메모

## 취약 예시 ##
<div class="comment">SUER_COMMENT</div>

댓글 저장 시 필터링/정제가 부실하면 Stored XSS

3. Dom-based XSS

서버 응답 자체보다, 브라우저의 JS 코드가 URL,입력값,postMessage 같은 값을 읽어서 DOM에 위험하게 넣을 때 발생합니다.

  • DOM : Document Object Model(문서 객체 모델)

    웹페이지(HTML)를 컴퓨터가 이해하기 쉽게 구조로 바꿔놓은 것

      <html>
        <body>
          <h1>안녕</h1>
          <p>내용</p>
        </body>
      </html>
    

    위 HTML을 브라우저는 아래 트리 구조와 같이 이해

      html
       └── body
            ├── h1 ("안녕")
            └── p ("내용")
    

    이 구조 전체가 DOM

    • DOM은 JS가 웹페이지를 조작할 수 있게 해줌

      예) 글자 바꾸기, 버튼 클릭하면 내용 변경, 팝업

        document.querySelector("h1").textContent = "바뀐 제목";
      
## 취약 예시 ##
const q = new URLSearchParams(location.search).get("q");
document.getElementById("result").innerHTML = q;

서버가 아니라 브라우저 JS가 직접 취약

PoC 예시

  1. HTML 삽입 여부 확인

     <b>teset</b>
    
  2. 이벤트 핸들러 실행 여부 확인

     <img src=x onerror=alert(1)>
    
  3. DOM XSS 기본 확인

     const payload = '<img src=x onerror=alert(1)>';
     target.innerHTML = payload;
    

자주 보이는 위험 Sink

sink : 외부 입력값이 “실제로 실행되거나 반영되는 최종 지점”

[사용자 입력] -> (가공/처리) -> [sink]

sink는 위험이 실제로 터지는 지점

대표적인 sink

  • innerHTML : 요소 안쪽 HTML을 통째로 바꿔버림

      div.innerHTML = userInput;
    
    • 문자열을 HTML로 해석
    • <script> 같은 것도 실행될 수 있음
  • outerHTML : 요소 자기 자신까지 포함해서 바꿔버림

      div.outerHTML = "<p>바뀜</p>";
    
    • innerHTML보다 더 강력
    • 역시 HTML로 해석됨
  • insertAdjacentHTML : 특정 위치에 HTML 삽입

      div.insertAdjacentHTML("beforeend", userInput);
    
    • 문자열을 그대로 HTML로 넣음
    • 위치만 다를 뿐 innerHTML과 동일하게 위험
  • document.write() : 페이지에 직접 HTML을 써버림

      document.write(userInput);
    
    • 옛날 방식이지만 여전히 위험
    • HTML + 스크립트 실행 가능
  • eval() : 문자열을 JavaScript 코드로 실행

      eval(userInput);
    
    • 문자열 → 코드로 변환해서 실행
    • XSS뿐 아니라 RCE까지 이어질 수 있음
  • new Function() : 문자열로 함수 생성 후 실행

      new Function(userInput);
    
    • eval과 거의 동일한 위험성
    • 동적으로 코드 실행
  • 문자열 기반 seTimeout(), setInterval()

      setTimeout("alert(1)", 1000);
    
    • 문자열을 코드처럼 실행함
    • 함수 대신 문자열 쓰면 위험

    안전한 방식

      setTimeout(() => alert(1), 1000);
    

예)

취약한 코드

box.innerHTML = userInpurt;

안전한 방식

box.textContent = userInput;

또는

const el = document.createElement("div"):
el.textContent = userInput;
box.appendChild(el);

→ 실무 감각으로

  • HTML로 넣지 말고 text로 넣어라
  • 문자열을 실행하지 말고 DOM API로 조립해라

방어 방법

  1. 출력 시 컨텍스트 맞춤 인코딩
    • HTML 본문: HTML entity encoding
    • 속성 : 속성 컨텍스트에 맞는 인코딩 + 반드시 따옴표 사용
    • URL: URL 검증 + 허용 스킴만 허용(http, https)
    • JS 문자열: JS 컨텍스트 인코딩
    • CSS/스크립트 컨텍스트 직접 삽입은 가능하면 피하기
  2. 안전한 DOM API 사용
    • tesxtContent
    • createElement
    • setAttribute
    • appendChild
  3. HTML Sanitization

    사용작 HTML 입력을 정말 허용해야 할 때만 sanitizer를 씀

    OWASP는 대표적으로 DOMPurify 사용을 권장

    단, sanitizer도 지속 업데이트가 중요

  4. 프레임워크의 위험 escape hatch 주의
    • React: dangerouslySetInnerHTML
    • Angular: bypassSecurityTrustAs*
    • Lit: unsafeHTML
  5. CSP는 보조 수단

    CSP는 좋지만 주 방어책이 아님 (OWASP도 언급)

  6. 최신 방어 포인트: Trusted Types

    특히 DOM XSS 방어에 유용

     Content-Security-Policy: require-trusted-types-for 'script'; trusted-types app-policy;
    

    이렇게 두면 innerHTML 같은 위험 sink에 문자열을 바로 넣는 습관을 강하게 통제할 수 있음

헷갈리기 쉬운 포인트

  • 입력 검증만으로 XSS 막을 수 없음
  • <script> 차단 = XSS 방어 완료”가 아님
  • 서버가 안전해 보여도 클라이언트 JS 때문에 DOM XSS가 날 수 있음
  • 템플릿 엔진 auto-escape가 있어도 속성/URL/JS 컨텍스트는 별도 사고가 남
  • sanitizer를 썼더라도, 이후 문자열을 다시 조작하면 방어가 무력화될 수 있음

실전 분석 순서

  1. 입력 지점 찾기
    • URL 파라미터
    • 폼 입력
    • 저장되는 게시물/댓글
    • 해시(#)
    • postMessage
    • 로컬 스토리지/세션 스토리지
  2. 반영 위치 확인
    • HTML 본문인가
    • 속성인가
    • JS 문자열인가
    • DOM API인가
  3. sink 확인
    • innerHTML 계열인지
    • 실행함수(eval) 계열인지
    • URL 이동/삽입인지
  4. 방어 확인
    • 인코딩이 컨텍스트에 맞는지
    • sanitizer를 쓰는지
    • Trusted Types/CSP가 있는지
    • 프레임워크 escape hatch를 쓰는지

요즘 실무에서 특히 중요한 XSS 포인트

  • 단순히 <script>...</script> 만 막는다고 끝나지 않음
  • 요즘은 DOM XSS, 프레임워크의 escape hatch, sanitizer 우회, 위험한 sink 사용이 더 중요
  • OWASP는 React의 dangerouslySetInnerHTML, Angular의 bypassSecurityTrustAs*, Lit의 unsageHTML 같은 지점을 대표 위험 지점으로 봄
  • MDN 기준으로 Trusted Types 관련 CSP(require-trusted-types-for, trusted-types)는 2026년 2월부터 최신 브라우저군에서 Baseline 2026으로 표시되고 있어, 최신 프론트엔드 방어에서 중요성이 더 커짐. 다만 구형 브라우저는 별도 고려가 필요
  • OWASP는 CSP/WAF만으로 XSS를 막으려 하지 말고, 컨텍스트별 출력 인코딩 + 안전한 DOM API + HTML Sanitization을 기본으로 하라고 권장

  • CSP (Content Security Policy)

    웹사이트에서 “어떤 코드만 실행해도 되는지” 정해놓는 보안 규칙

    = 출입 가능한 코드만을 미리 정해놓는 출입통제 시스템

      <script src="https://evil.com/hack.js"></script>
    

    다음과 같은 XSS 스크립트 삽입 공격이 있을 때 CSP가 있으면 evil.com 을 차단한다.

    Trusted Types

    위험한 HTML/스크립트를 아무 문자열로 넣지 못하게 막는 규칙

      element.innerHTML = userInput;
    

    만약 이 JS 코드에서 userInput이 아래와 같으면

      <script>alert(1)</script>
    

    바로 XSS 발생!!