JSON Toolkit

JSON을 통한 XSS는 어떻게 일어나나

핵심 요약: JSON은 스스로 실행되지 않습니다. XSS는 이를 HTML로 취급하는 순간 일어납니다. 서버 측 문자열 연결, 프런트엔드 innerHTML, 또는 JSON을 스크립트 태그에 직접 인라인할 때입니다.

중급약 4분 소요2026-10-30 발행2026-10-30 업데이트
JSON이 XSS가 되는 세 가지 길과 예방법

핵심부터 말하면, JSON은 스스로 실행되지 않습니다. 먼저 세 출력 경로를 닫으세요. 서버 측은 문맥에 따라 이스케이프하고, innerHTML 대신 textContent를 쓰며, JSON을 인라인할 때 <를 \u003c로 바꾸세요.

{"name": "<img src=x onerror=alert(1)>"}는 유효한 데이터입니다. 위험한지는 전적으로 어디에 넣느냐에 달려 있습니다.

진입점 하나: 서버 측 문자열 구성

res.send(`<div>Hello, ${data.name}</div>`);

브라우저가 data.name 안의 태그를 파싱합니다. 여기서 JSON은 컨테이너일 뿐이고 잘못은 틀린 문맥에 이스케이프한 것입니다.

수정: 템플릿 엔진의 기본 이스케이프(EJS의 <%= %>, Vue와 Django의 {{ }})를 쓰세요. 사용자 제어 값에 <%- %>, v-html, |safe를 피하세요.

진입점 둘: innerHTML

el.innerHTML = user.bio;    // parses markup
el.textContent = user.bio;  // inserts text

React의 {user.bio}와 Vue의 {{ user.bio }}는 텍스트 노드를 만들어 기본으로 안전합니다. dangerouslySetInnerHTML과 v-html은 명시적 옵트아웃입니다.

리치 텍스트가 진짜 필요하면 백엔드를 신뢰하지 말고 허용 목록(DOMPurify 등)으로 정화하세요.

진입점 셋: 스크립트 태그에 인라인

<script>
  window.__DATA__ = {"name": "x</script><script>alert(1)</script>"};
</script>

데이터 안의 </script>가 블록을 조기 종료하고 그 뒤 모든 것이 새 스크립트로 실행됩니다. 가장 많이 놓치는 경우입니다.

JSON.stringify(data)
  .replace(/</g, '\\u003c')
  .replace(/>/g, '\\u003e')
  .replace(/&/g, '\\u0026');

여전히 유효한 JSON이고 파싱 후 동일하며, 태그를 닫지 못합니다. Next.js와 Nuxt가 __NEXT_DATA__와 __NUXT__에 같은 일을 합니다.

알아 둘 두 인접 문제

JSON 하이재킹 — 옛 브라우저는 <script src>로 로드한 JSON을 실행했고 Array를 재정의해 내용이 유출되었습니다. 현대 브라우저는 고쳤고, Content-Type: application/json과 X-Content-Type-Options: nosniff가 나머지를 닫습니다.

JSONP 잔재 — 사용자 제어 콜백 이름은 설계상 임의 코드 실행입니다. 신규 시스템에서 쓰지 마세요.

체크리스트

  • JSON 필드가 HTML에 도달하는 모든 곳이 문맥(HTML, 속성, JS, URL)에 맞게 이스케이프됨
  • API 문자열을 소비하는 innerHTML, v-html, dangerouslySetInnerHTML이 없음
  • 인라인 JSON이 <, >, &를 이스케이프함
  • 리치 텍스트가 거부 목록 필터가 아니라 허용 목록 정화를 거침
  • 응답이 Content-Type: application/json과 nosniff를 지님

FAQ

유효한 JSON이 왜 여전히 XSS를 일으킬 수 있나?

JSON은 텍스트일 뿐이고 실행되지 않으며, 유효하다는 것은 파싱된다는 뜻일 뿐입니다. 위험은 HTML이나 스크립트 문맥에 쓰일 때 나타납니다. 브라우저가 태그를 파싱하거나 </script>가 블록을 조기 종료하고 나머지가 코드로 실행됩니다. 잘못은 데이터 형식이 아니라 출력 시 문맥 인식 이스케이프의 부재입니다.

API 필드를 렌더할 때 textContent와 innerHTML 중 무엇을 써야 하나?

textContent를 선호하세요. 값을 평문으로 삽입해 태그가 그대로 표시됩니다. innerHTML은 마크업을 파싱해 <img onerror=...>가 든 API 필드가 실행됩니다. React의 {user.bio}와 Vue의 {{ user.bio }}는 기본으로 텍스트 노드를 만들고, dangerouslySetInnerHTML과 v-html은 명시적 옵트아웃입니다.

<script> 태그에 인라인한 JSON은 어떻게 이스케이프해야 하나?

예. 데이터 안의 </script>가 블록을 조기 종료하고 그 뒤 모든 것이 새 스크립트로 실행됩니다. 직렬화 후 <, >, &를 \u003c, \u003e, \u0026로 바꾸세요. 결과는 여전히 유효한 JSON이고 동일하게 파싱되며, 태그를 닫지 못합니다.

더 읽을거리