모든 웹 보안 강연은 결국 같은 슬라이드에 도달합니다: HTML에 삽입하기 전에 사용자 입력을 항상 이스케이프하세요. 그 이유는 HTML 엔티티입니다 — <script>를 <script>로 변환하고 사용자 댓글이 XSS 공격이 되는 것을 방지하는 메커니즘입니다.
하지만 이스케이프는 절반에 불과합니다. 언이스케이프 — &를 &로 다시 변환 — 도 똑같이 중요하고 잘못하면 똑같이 위험합니다. 이 게시물은 양쪽 방향, 중요한 문자, 그리고 실제 취약점으로 이어지는 실수를 다룹니다.
중요한 5문자
HTML에는 구조적으로 중요한 5문자가 있습니다 — 태그, 속성, 엔티티를 구분합니다.这些中任何一个이 사용자 콘텐츠에 나타나고 이스케이프되지 않으면 브라우저는它们을 HTML 마크업으로 해석합니다:
| 문자 | 엔티티 | 중요한 이유 |
|---|---|---|
< |
< |
태그를 시작합니다. <script>는 태그입니다. |
> |
> |
태그를 끝냅니다. |
& |
& |
엔티티를 시작합니다. <는 엔티티입니다. |
" |
" |
속성 값을 구분합니다. |
' |
' |
단일 따옴표로 둘러싸인 속성을 구분합니다. |
HTML 요소의 콘텐츠에 텍스트를 삽입하는 경우 <, >, &를 이스케이프해야 합니다. 속성 값에 텍스트를 삽입하는 경우 "(또는 속성이 단일 따옴표를 사용하는 경우 ’`)도 이스케이프해야 합니다.
엔티티 작동 방식
HTML 엔티티는 브라우저가 단일 문자로 해석하는 특수 시퀀스입니다:
- 이름 있는 엔티티:
&,<,>,", — 읽기 쉽고, 제한된 세트. - 숫자 엔티티:
<(10진수),<(16진수) — 코드 포인트별 모든 유니코드 문자. - 기호용 이름 있는 엔티티:
©(©),€(€),—(—) — 편의 이름.
& 엔티티는 특별합니다. & 자체의 이스케이프이기 때문입니다. 먼저 &를 이스케이프하고, 그 다음 <와 >를 이스케이프하면 이중 이스케이프되지 않습니다. 순서가 중요합니다:
올바름: & → & → &lt; (올바름)
잘못됨: < → < → &lt; (이중 이스케이프)
항상 먼저 &를 이스케이프하세요. 그런 다음 다른 문자를. 그렇게 하면 이중 이스케이프하지 않습니다.
이스케이프가 필요한 곳
사용자 생성 콘텐츠. 댓글, 포럼 게시물, 리뷰, 메시지 — 사용자에게서 와서 페이지에 나타나는 모든 텍스트는 이스케이프해야 합니다. <img src=x onerror=alert(1)>를 포함하는 댓글은 <와 >를 이스케이프하지 않으면 XSS 공격입니다.
속성의 데이터. 사용자 데이터를 href, src, alt 또는 title 속성에 넣는 경우 엔티티 문자를 이스케이프하세요. foo" onclick="alert(1)와 같은 값은 속성을 빠져나와 이벤트 핸들러를 주입합니다.
JavaScript 문자열 보간. JavaScript 내부에 HTML를 구축하는 경우:
// 위험: 원시 보간
element.innerHTML = `<p>${userComment}</p>`;
// 안전: 이스케이프된 보간
element.innerHTML = `<p>${escapeHtml(userComment)}</p>`;
innerHTML은 문자열을 HTML로 해석합니다. userComment에 마크업이 포함되어 있으면 실행됩니다. HTML에 보간하기 전에 항상 이스케이프하세요.
서버사이드 렌더링. 템플릿 엔진(Handlebars, Jinja, EJS, Razor)은 일반적으로 기본적으로 자동 이스케이프합니다. 하지만 “safe” 또는 “raw” 필터(Handlebars의 삽입 중괄호 {{{이나 Django의 |safe)를 사용하는 경우 이스케이프를 옵트아웃하고 있습니다. 콘텐츠를 제어할 때만 그렇게 하세요.
언제 언이스케이프할지
언이스케이프는 프로세스를 반전시킵니다: < → <, & → &. 필요한 경우:
- 인코딩된 데이터 표시. API가 이스케이프된 HTML를 반환하는 경우(일부는 렌더링을 방지하기 위해这样做합니다), 표시하기 전에 언이스케이프해야 합니다.
- 폼 데이터 처리. 폼 제출에는 URL 인코딩되거나 엔티티 인코딩된 값이 포함될 수 있습니다.
- 사용자 입력 분석. 사용자가 HTML 인코딩된 텍스트를 붙여넣고 그대로 표시하려면 먼저 언이스케이프한 다음, HTML에 다시 삽입하는 경우 다시 이스케이프하세요.
위험: 표시할 때만 언이스케이프하고, 저장할 때는 절대 언이스케이프하지 마세요. 원래 이스케이프된 형태를 저장하세요. 렌더링 시에 언이스케이프하세요.
XSS: 이스케이프된 문자 하나뿐
크로스 사이트 스크립팅(XSS)은 이스케이프되지 않은 사용자 입력이 브라우저의 HTML 파서에 도달할 때 발생합니다. 가장 일반적인 변형:
저장형 XSS. 사용자가 <script>steal(document.cookie)</script>를 댓글로 제출합니다. 서버는它을 이스케이프하지 않고 저장합니다. 댓글을 보는 모든 방문자가 스크립트를 실행합니다.
반사형 XSS. URL에 ?q=<script>alert(1)</script>가 포함되어 있습니다. 서버는 q 매개변수를 이스케이프하지 않고 페이지에 반사합니다. 링크를 클릭하는 누구나 스크립트를 실행합니다.
DOM 기반 XSS. JavaScript가 location.hash를 읽고 innerHTML을 통해 DOM에 삽입합니다. 해시에 마크업이 포함되어 있습니다. 브라우저가它을 렌더링합니다.
세 가지 모두 같은 것으로 방지됩니다: HTML에 콘텐츠를 삽입하기 전에 <, >, &, ", '를 이스케이프하세요.
이스케이프 함수
모든 언어에 있습니다. 패턴은 동일합니다:
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
순서가 중요합니다: 먼저 &, 그런 다음 다른 문자를. & 전에 <를 이스케이프하면 < 대신 &lt;가 됩니다.
고throughput 시스템의 경우 라이브러리를 사용하세요. DOMPurify는 HTML을 정화합니다(안전한 태그를 허용하고 위험한 것을 제거). 웹 플랫폼의 내장 DOMParser와 textContent도 암묵적으로 이스케이프를 처리합니다.
일반적인 실수
<뒤에&이스케이프 — 이중 이스케이프를 유발합니다:<대신&lt;.- 속성 컨텍스트 잊기 —
<와>를 이스케이프하지만"를 이스케이프하지 않으면 속성 주입이 가능합니다. - 이스케이프 없이
innerHTML사용 — 가장 일반적인 XSS 벡터. - 서버 응답 신뢰 — “정화된” HTML를 반환하는 API는 모든 것을 정화하지 않을 수 있습니다. 클라이언트 측에서도 이스케이프하세요.
- 너무 일찍 언이스케이프 — 저장 시 언이스케이프하면 원시 페이로드가 데이터베이스에 남아, 렌더링이 다시 이스케이프하는 것을 잊으면 실행 준비가 됩니다.
시도해보기
디코딩해야 하는 HTML 인코딩된 텍스트나 HTML에 안전하게 삽입해야 하는 원시 텍스트가 있다면 브라우저 기반 도구를 사용하여 변환이 로컬에서 유지되도록 하세요.