Reflected 크로스사이트 스크립팅 (Reflected XSS)

Reflected Cross-Site Scripting (XSS)

설명

Reflected XSS는 요청 파라미터, 헤더 등 신뢰되지 않은 사용자 입력이 HTML 응답에 적절한 컨텍스트 인코딩 없이 그대로 반영될 때 발생합니다. 브라우저는 이 값을 HTML/JS로 해석하므로, 공격자는 스크립트를 주입해 사용자의 브라우저에서 임의 코드를 실행시킬 수 있습니다. 예를 들어 검색어, 에러 메시지, 프로파일 화면 등에 <script> 또는 이벤트 핸들러(onerror 등)를 삽입하여 세션 쿠키 탈취, 사용자 가장, 악성 사이트로 리다이렉트 등을 유도할 수 있습니다.

잠재적 영향

  • 세션 탈취(Session Hijacking): 악성 스크립트로 스크립트가 읽을 수 있는 쿠키/토큰을 빼내 계정 탈취 가능
  • 임의 스크립트 실행(Arbitrary JavaScript): 사용자의 브라우저 권한으로 임의 코드 실행
  • 피싱 및 리다이렉션(Phishing/Redirect): 가짜 로그인 폼 표시 또는 악성 페이지로 이동
  • 데이터 조작(UI Redress/DOM 조작): 화면 변조, 폼 자동 제출 등 사용자의 의도치 않은 행위 유도
  • 정보 유출(Data Exfiltration): 페이지 내 민감정보(이메일, CSRF 토큰 등) 수집 후 외부로 전송

해결 방법

  • 출력 인코딩: HTML 본문, 속성, URL 등 출력 위치에 맞는 인코딩을 적용하세요. URL은 허용할 스킴과 목적지도 검증해야 합니다. HTML 정화 도구는 실제 마크업을 허용할 때 사용하며, 텍스트 인코딩과는 다릅니다.
  • 템플릿 엔진 사용: auto-escape가 기본인 템플릿(예: Pug 등)을 사용하고, 신뢰되지 않은 데이터를 HTML 문자열에 직접 concat 하지 마세요.
  • 안전한 포맷 채택: 가능하면 res.json으로 JSON 응답을 사용하여 브라우저의 스크립트 해석 위험을 낮추세요. Content-Type을 명확히 설정하고, 클라이언트에서도 응답 값을 안전한 텍스트 API로 출력하세요.
  • 입력 검증: 필요 시 허용 목록(whitelist)으로 길이/문자열 패턴/허용 문자 제한을 적용하세요.
  • 보안 헤더: CSP(Content-Security-Policy), X-Content-Type-Options 등 보안 헤더를 추가 방어로 사용하세요. 이들은 출력 인코딩을 대신하지 않습니다.
  • 라이브러리 업데이트: express, 템플릿 엔진, 인코딩/정화 라이브러리를 최신 버전으로 유지하세요.

예시

변경 전

javascript
const express = require("express");
const app = express();

// 취약: 사용자 입력을 HTML에 그대로 삽입
app.get("/greet", (req, res) => {
  const name = req.query.name || "guest";
  // name 파라미터가 "<img src=x onerror=alert(1)>" 인 경우 스크립트 실행 가능
  res.type("text/html").send(`<h1>Hello ${name}</h1>`);
});

app.listen(3000);

변경 후

javascript
const express = require("express");
const he = require("he"); // HTML 인코딩 라이브러리
const app = express();

// 안전: 컨텍스트에 맞는 출력 인코딩 적용
app.get("/greet", (req, res) => {
  const rawName = typeof req.query.name === "string" ? req.query.name : "guest";
  const safeName = he.encode(rawName, { useNamedReferences: true }); // &, <, >, ", ' 등 인코딩
  res.type("text/html").send(`<h1>Hello ${safeName}</h1>`);
});

// 혹은 JSON 응답 사용
app.get("/greet.json", (req, res) => {
  const name = String(req.query.name || "guest");
  res.json({ message: "Hello", name });
});

app.listen(3000);

설명:

  • 변경 전: 사용자 입력을 HTML에 그대로 연결하여 브라우저가 이를 코드로 해석합니다. 공격자가 스크립트 태그나 이벤트 핸들러를 주입하면 사용자의 브라우저에서 악성 JavaScript가 실행되어 계정 탈취 및 피싱이 발생할 수 있습니다.
  • 변경 후: 응답에 포함하기 전 he.encode로 HTML 컨텍스트에 맞게 특수 문자를 엔티티로 변환하여 스크립트 실행을 차단했습니다. 또한 JSON 응답(res.json)을 사용하면 브라우저의 HTML 해석과 분리되어 XSS 위험을 더 낮출 수 있습니다.

참조