</>DevTools

&;HTML 엔티티 변환

HTML 엔티티 인코딩/디코딩

HTML 엔티티: 왜 필요하고 어디까지 변환해야 하는가

HTML 엔티티는 브라우저가 마크업으로 해석할 문자를 '글자 그대로 보여 달라'고 표시하는 방법입니다. &lt; 는 화면에 < 로 렌더링되지만 태그의 시작으로 취급되지 않습니다. 이 구분은 단순한 표기 문제가 아니라 XSS 방어의 핵심 원리이기도 합니다.

반드시 변환해야 하는 다섯 문자

HTML에서 특별한 의미를 갖는 문자는 사실 몇 개뿐입니다. 이 다섯 개만 정확히 처리하면 대부분의 문맥에서 안전합니다.

문자엔티티숫자 참조변환해야 하는 이유
<&lt;&#60;태그의 시작으로 해석됨
>&gt;&#62;태그의 끝으로 해석됨
&&amp;&#38;엔티티의 시작으로 해석됨
"&quot;&#34;속성 값을 감싼 큰따옴표를 조기 종료시킴
'&#39;&#39;속성 값을 감싼 작은따옴표를 조기 종료시킴

&apos; 를 쓰지 않는 이유

작은따옴표의 이름 있는 엔티티 &apos; 는 HTML5와 XML에는 정의되어 있지만 HTML4에는 없습니다. 그래서 오래된 파서나 일부 이메일 클라이언트에서는 그대로 문자열 &apos; 로 표시될 수 있습니다. 숫자 참조 &#39; 는 모든 버전에서 동작하므로, 라이브러리들이 관례적으로 &#39; 를 씁니다.

사용 방법

  1. 변환할 텍스트, 또는 되돌릴 엔티티가 포함된 문자열을 입력합니다.
  2. Encode는 특수문자를 엔티티로 바꾸고, Decode는 엔티티를 원래 문자로 되돌립니다.
  3. HTML 소스에 붙여 넣을 문자열이라면 Encode 결과를 사용합니다.
  4. 웹에서 긁어온 데이터에 &amp;lt; 같은 값이 섞여 있다면 Decode로 정리합니다.

이스케이프는 문맥에 따라 달라진다

엔티티 변환은 'HTML 본문' 문맥에서만 충분합니다. 같은 값이 다른 문맥에 들어가면 다른 규칙이 필요하고, 이를 혼동하면 이스케이프를 했는데도 취약점이 남습니다.

  • HTML 본문: 위 다섯 문자를 엔티티로. 이 도구가 하는 일입니다.
  • 속성 값: 따옴표로 반드시 감싸고 그 따옴표 문자를 이스케이프. 따옴표 없이 쓰면 공백만으로도 새 속성을 주입할 수 있습니다.
  • <script> 내부: 엔티티가 해석되지 않습니다. JavaScript 문자열 이스케이프나 JSON.stringify를 써야 하며, </script> 시퀀스도 따로 막아야 합니다.
  • URL 속성(href, src): 엔티티만으로는 javascript: 스킴을 막지 못합니다. 스킴 허용 목록 검증이 필요합니다.
  • CSS 안: 별도의 CSS 이스케이프 규칙을 씁니다.

이중 인코딩 문제

&amp;lt; 처럼 &가 한 번 더 인코딩된 형태를 흔히 보게 됩니다. 이는 이미 엔티티로 변환된 문자열을 다시 변환했을 때 생깁니다. 화면에는 &lt; 라는 글자가 그대로 나타나 사용자 눈에 바로 띕니다.

원인은 대개 계층이 겹치는 것입니다. 템플릿 엔진이 자동으로 이스케이프하는데 애플리케이션 코드에서 한 번 더 이스케이프하거나, DB에 이스케이프된 값을 저장한 뒤 출력 시 또 이스케이프하는 경우입니다. 원칙은 '저장은 원본으로, 이스케이프는 출력 직전에 한 번만'입니다. React, Vue, Django, Rails 같은 현대 프레임워크는 기본적으로 자동 이스케이프하므로 수동 변환을 덧붙일 필요가 없습니다.

이런 곳에 씁니다

  • 코드 예제를 블로그 글에 넣을 때 태그가 실제로 렌더링되지 않게 처리
  • 웹 스크래핑 결과에 남은 &amp;#39; 같은 잔여물 정리
  • 이메일 HTML 템플릿에 사용자 이름 등 동적 값을 삽입
  • CSV나 XML로 내보낼 데이터에서 &와 < 처리
  • 레거시 시스템이 남긴 이중 인코딩 데이터의 원본 복구

자주 묻는 질문

엔티티 변환만 하면 XSS를 막을 수 있나요?
HTML 본문에 텍스트를 넣는 문맥에서는 대체로 충분하지만, 그것만으로 XSS 전체가 막히지는 않습니다. 속성 값, <script> 내부, href의 URL, 인라인 CSS는 각각 다른 이스케이프 규칙을 요구합니다. 게다가 사용자가 제공한 HTML을 그대로 렌더링해야 하는 기능(리치 텍스트 에디터 등)이라면 이스케이프가 아니라 DOMPurify 같은 새니타이저로 허용 태그만 남기는 방식이 맞습니다.
&nbsp; 는 그냥 공백과 다른가요?
다릅니다. non-breaking space(U+00A0)는 줄바꿈이 일어나지 않는 공백이고, 브라우저가 연속된 일반 공백을 하나로 합치는 것과 달리 그대로 유지됩니다. 그래서 간격 조절에 남용되곤 하는데, 간격은 CSS margin이나 padding으로 처리하는 편이 옳습니다. 또 텍스트 비교나 검색에서 일반 공백과 다른 문자로 취급되어 버그의 원인이 되기도 합니다.
이름 있는 엔티티와 숫자 참조 중 무엇을 쓸까요?
HTML 문서라면 &lt; 같은 이름 엔티티가 읽기 쉽습니다. XML이나 그 계열 형식으로 내보낼 때는 &#60; 같은 숫자 참조가 안전합니다. XML이 기본으로 정의하는 이름 엔티티는 lt, gt, amp, quot, apos 다섯 개뿐이고 &nbsp; 등은 정의되어 있지 않아 파싱 오류가 납니다.
한글이나 이모지도 엔티티로 바꿔야 하나요?
필요 없습니다. 문서가 UTF-8이고 <meta charset="utf-8">이 선언되어 있다면 한글, 일본어, 이모지 모두 그대로 쓰면 됩니다. 엔티티로 바꾸면 용량만 크게 늘고 가독성이 떨어집니다. 문자 인코딩을 지정할 수 없는 아주 오래된 환경에서만 고려할 문제입니다.
Decode하면 원래 문자열이 정확히 복원되나요?
대개 그렇지만 완전한 왕복 보장은 아닙니다. 예를 들어 &#65; 와 A 는 디코딩 후 같아지므로, 어느 쪽이 원본이었는지는 알 수 없습니다. 또 이중 인코딩된 문자열은 몇 번 디코딩해야 원본인지 데이터만 보고 판단하기 어렵습니다.
React에서 직접 이스케이프해야 하나요?
아닙니다. JSX에 넣은 문자열은 React가 자동으로 이스케이프합니다. 오히려 수동으로 엔티티를 넣으면 &lt; 라는 글자가 화면에 그대로 보입니다. 예외는 dangerouslySetInnerHTML을 쓸 때인데, 이때는 이스케이프가 아니라 새니타이징이 필요합니다.

💡 참고: 화면에 &lt; 나 &amp; 같은 글자가 그대로 보인다면 이스케이프가 두 번 적용된 것입니다. 이스케이프를 하는 계층이 몇 곳인지부터 확인하세요.

🔗관련 도구🔄 텍스트/데이터 변환