노트

XSS

Cross-Site Scripting

프런트엔드#security#browser · 연결된 개념 8개

쉽게 말하면

XSS는 공격자가 댓글 같은 입력에 몰래 스크립트를 넣어, 그게 내 사이트 코드인 척 다른 사용자 브라우저에서 실행되게 하는 공격이에요. 이미 집 안에 들어온 가짜 식구라 동일 출처 정책이 문을 지켜도 소용없죠.

비유가 깨지는 곳 가짜 식구를 문 앞에서 다 걸러 내긴 어려워서 출력할 때 맥락에 맞게 인코딩하는 게 기본이에요. CSP는 정책에 없는 스크립트 실행을 막는 마지막 방어선일 뿐, 인코딩과 새니타이즈를 대신하지 못해요.

XSS(Cross-Site Scripting)는 공격자가 넣은 스크립트가 내 사이트의 출처(origin) 권한으로 사용자 브라우저에서 실행되는 공격이다. 같은 출처의 코드로 돌기 때문에 동일 출처 정책가 막아 주지 못한다. 그 스크립트는 페이지를 바꾸고, 로그인한 사용자 행세를 하며 요청을 보내고, JS가 읽을 수 있는 데이터(localStorage의 토큰 등)를 빼 갈 수 있다.

어디서 들어오나

유형경로예
저장형(Stored)서버에 저장된 입력이 다른 사용자에게 그대로 출력됨게시글·댓글·프로필 이름
반사형(Reflected)요청 값이 응답에 바로 되돌아옴검색어를 "○○ 검색 결과"로 출력
DOM 기반(DOM-based)서버를 거치지 않고 클라이언트 JS가 입력을 위험한 곳에 넣음location.hash를 innerHTML에 대입

공통점은 "데이터로 받은 문자열이 코드로 해석되는 곳(sink)에 들어간다"는 것이다. innerHTML, document.write, eval, <a href="javascript:...">, 인라인 이벤트 속성(onerror=)이 대표적이다.

막는 법

  1. 출력 맥락에 맞게 인코딩: HTML 본문에는 <·>·&·따옴표를 엔티티로, 속성·URL·JS 문자열에는 각 맥락의 규칙으로 바꾼다. 입력 단계에서 걸러내는 것보다 출력 단계에서 바꾸는 것이 기본이다
  2. 위험한 API를 피한다: 텍스트는 textContent로 넣는다. React는 JSX의 {값}을 자동으로 이스케이프하므로 대부분 안전하지만, dangerouslySetInnerHTML은 예외다. href에 사용자 입력으로 들어온 javascript: URL은 React 18까지 경고만 하고 그대로 실행됐지만, React 19부터는 실행되지 않는 URL로 바꿔 막는다. 그래도 URL은 https: 같은 허용 스킴인지 직접 검사하는 편이 안전하다
  3. HTML을 꼭 받아야 하면 새니타이즈: 서식 있는 편집기 결과처럼 HTML을 허용해야 할 때는 DOMPurify 같은 검증된 라이브러리로 허용 목록 밖의 태그·속성을 지운다. 정규식으로 직접 거르지 않는다
  4. 피해를 줄이는 장치: 세션 쿠키에 HttpOnly를 주면 스크립트가 쿠키를 읽지 못한다 → HTTP 쿠키. 다만 스크립트가 사용자 대신 요청을 보내는 것까지 막지는 못한다

CSP: 실행 자체를 제한하는 마지막 방어선

CSP(Content Security Policy)는 응답 헤더로 "이 페이지에서 어떤 출처의 스크립트를 실행해도 되는지"를 브라우저에 알리는 정책이다. 인코딩을 놓쳐 스크립트가 주입돼도 정책에 없는 스크립트는 실행되지 않는다.

Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'
  • 요청마다 새로 만든 예측 불가능한 nonce를 <script nonce="r4nd0m">처럼 붙인 스크립트만 실행된다. 'strict-dynamic'은 그렇게 믿은 스크립트가 새로 불러오는 스크립트까지 신뢰를 넘겨 준다. nonce 없는 인라인 스크립트, 인라인 이벤트 속성, javascript: URL, eval은 막힌다
  • object-src 'none'은 플러그인(<object>·<embed>) 경로를, base-uri 'none'은 <base> 태그로 상대 경로 스크립트의 출처를 바꾸는 공격을 막는다
  • 도메인 허용 목록 방식은 바르게 짜기 어렵고 우회에 쓰일 수 있는 도메인이 섞이기 쉬워서, nonce나 해시 기반의 "엄격한 CSP(Strict CSP)"가 권장된다
  • 처음에는 Content-Security-Policy-Report-Only 헤더(보고 받을 곳은 report-to 지시어로 지정)로 차단 없이 위반만 보고받으며 깨지는 곳을 찾는다
  • CSP는 보완책이다. 인코딩·새니타이즈를 대신하지 않는다

CSRF와의 차이

CSRF는 다른 사이트가 사용자 브라우저를 시켜 요청을 보내게 하는 공격이고, XSS는 내 사이트 안에서 공격자 코드가 도는 공격이다. XSS가 있으면 페이지 안의 CSRF 토큰도 읽을 수 있어 CSRF 방어가 무력해진다. 토큰을 어디에 둘지의 트레이드오프는 JWT, 다른 주입형 공격은 프로토타입 오염을 본다.

명세: W3C — Content Security Policy Level 3: script-src · W3C — CSP Level 3: 'strict-dynamic' · W3C — CSP Level 3: base-uri · W3C — CSP Level 3: Content-Security-Policy-Report-Only 출처: MDN — Cross-site scripting (XSS) · MDN — Content Security Policy: Strict CSP · OWASP — Cross Site Scripting Prevention Cheat Sheet · React — Dangerously setting the inner HTML · React 소스 — sanitizeURL (v19.0.0)

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • HTTP와 HTTPS

    HTTPS는 HTTP(Hypertext Transfer Protocol) 메시지를 TLS(Transport Layer Security)로 감싸 보내는 방식이다. HTTP 자체는 바뀌지 않고, 그 아래에 암호화 계층이 하나 끼어든다. 기본 포트는 HTTP가 80, HTTPS가 443이다.

  • 브라우저 요청과 서버 간 요청

    같은 HTTP 요청이라도 브라우저에서 보내면 여러 보안 규칙이 강제되고, 서버에서 보내면 아무 제한이 없다. 브라우저 보안 정책은 신뢰할 수 없는 코드로부터 사용자를 지키려고 있기 때문이다.

  • 서버 전송 이벤트(SSE)

    서버 전송 이벤트(Server-Sent Events, SSE)는 HTTP(HyperText Transfer Protocol) 응답 하나를 닫지 않고 열어 둔 채, 서버가 원할 때마다 텍스트 이벤트를 흘려보내는 서버 → 클라이언트 단방향 푸시 방식이다. HTML 표준에 정의돼 있고 브라우저는 EventSource로 받는다.

  • CSR bailout

    CSR(Client-Side Rendering) bailout은 미리 렌더(Prerender)되는 라우트에서 서버가 알 수 없는 값을 읽는 클라이언트 컴포넌트를 만나, 가장 가까운 Suspense 경계까지의 클라이언트 컴포넌트 트리를 서버 렌더에서 포기하고 브라우저 렌더로 넘기는 일이다. 대표 원인이 useSearchParams()다. 빌드 때는 ?q=신발 같은 쿼리를 모르기 때문이다.

  • SEO

    SEO(Search Engine Optimization)는 검색 엔진이 페이지를 잘 찾고(크롤링, crawling), 이해해서 저장하고(색인, indexing), 알맞은 검색어로 노출하도록(순위, ranking) 사이트를 다듬는 일이다. 개발자가 맡는 부분은 대부분 앞의 두 단계, 즉 "검색 엔진이 읽을 수 있게 만드는 것"이다.

보기 옵션