
외부 API에서 받아온 제목을 블로그 화면에 그대로 넣으면, 그 안의 태그가 진짜 태그로 동작합니다. 남이 넣은 문장이 내 페이지에서 실행된다는 뜻입니다. 이 글은 짧은 함수 두 개로 이걸 막은 방법입니다.
글자가 글자로 안 들어간다
제가 만든 지원금 페이지는 공공데이터에서 받아온 제목과 설명을 화면에 뿌립니다. 이걸 innerHTML로 넣었습니다. 편하니까요.
문제는 innerHTML이 받은 문자열을 HTML로 해석한다는 점입니다. 제목에 <script>가 들어 있으면 그게 진짜 스크립트가 됩니다. 공공데이터에 그런 게 들어 있진 않겠지만, “들어 있지 않겠지”에 기대는 게 문제입니다.
다섯 글자만 바꾼다
막는 방법은 간단합니다. HTML에서 특별한 뜻을 가진 글자를 표시용 코드로 바꿔치기 하면 됩니다.
function escapeHTML(value){ return String(value == null ? '' : value) .replace(/[&<>"']/g, function(c){ return { '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]; });}바꾸는 순서에 이유가 있습니다. &를 가장 먼저 처리해야 합니다. 나중에 하면 앞에서 만든 <의 &까지 다시 바꿔서 &lt;가 됩니다. 정규식에 &를 맨 앞에 둔 게 그래서입니다.
null 처리도 넣었습니다. 데이터가 비어 있을 때 화면에 “null”이라는 글자가 그대로 찍히는 걸 막습니다.

주소는 따로 봐야 한다
여기서 놓치기 쉬운 게 있습니다. 링크 주소는 escapeHTML로 막을 수 없습니다. javascript:로 시작하는 주소는 특수문자가 없어도 클릭하면 실행됩니다.
function safeURL(url){ var value = String(url||''); return /^(https?:\/\/|\/)/i.test(value) ? value : '#';}방식이 반대입니다. escapeHTML은 나쁜 것을 바꾸는 쪽이고, safeURL은 좋은 것만 통과시키는 쪽입니다. 나쁜 것 목록을 만들면 빠뜨리기 쉬우니, 주소처럼 형태가 정해진 값은 허용 목록이 낫습니다.
http://, https://, 그리고 /로 시작하는 내부 주소만 통과시키고 나머지는 #으로 바꿉니다. 링크가 죽는 게 실행되는 것보다 낫습니다.

공공데이터인데도 거르는 이유
“정부 API인데 그런 게 들어 있겠나”라고 생각했습니다. 실제로 안 들어 있을 겁니다. 그래도 거르기로 한 이유는 데이터가 어디서 오는지가 나중에 바뀔 수 있기 때문입니다.
지금은 공공데이터만 씁니다. 나중에 다른 출처를 붙이거나, 이 코드를 다른 곳에 복사해 쓸 수 있습니다. 그때 거르는 함수가 이미 들어 있는 것과 없는 것은 차이가 큽니다.
한계
이 두 함수로 모든 경우를 막지는 못합니다. 값을 태그 속성 안에 넣거나 스타일에 넣는 경우는 규칙이 다릅니다. 제 화면은 글자와 링크만 다뤄서 이 정도로 충분했지만, 더 복잡한 화면이라면 부족합니다.
그리고 제가 보안 전문가는 아닙니다. 실제 공격을 시도해 검증하지는 않았고, 알려진 기본 방식을 그대로 적용한 수준입니다.
공식 문서는 이 문제를 어떻게 설명하나
위에서 한 방식이 개인 경험에만 기대지 않도록, 브라우저 문서(MDN)와 웹 보안 지침(OWASP)이 같은 문제를 어떻게 다루는지 옮겨 둡니다.
MDN의 innerHTML 문서는 이 속성을 사용자가 준 안전하지 않은 문자열이 정리되지 않은 채 화면에 들어가는 대표적인 통로로 설명합니다. 문자열에 악성 내용이 들어 있을 수 있다면 직접 대입하는 것이 보안 위험이라고도 적혀 있습니다. 또 innerHTML로 넣은 <script> 요소는 실행되지 않지만, 그렇다고 안전한 것은 아니라고 합니다. 문서에 실린 예가 <img src='x' onerror='alert(1)'>입니다. 스크립트 태그가 아니어도 이미지 태그의 오류 처리 속성으로 코드가 실행될 수 있습니다. 위에서 <script>만 막는 것으로는 부족한 이유가 이것입니다.
그냥 글자라면 textContent가 더 간단하다
받은 값이 처음부터 순수한 글자여야 한다면 변환 함수를 쓰는 대신 textContent에 넣는 방법이 있습니다. MDN은 사용자가 준 내용이 일반 텍스트여야 한다는 것을 알고 있을 때 textContent를 쓰라고 하며, 이러면 HTML로 해석되지 않는다고 설명합니다. 같은 문서에서 innerHTML로 글자를 읽거나 쓰는 것은 권하지 않는다고 하고, 그 이유로 원시 HTML을 다루기 때문에 XSS 공격에 노출될 수 있다는 점과, 글자만 넣는 경우에도 HTML 파서를 거치므로 더 느리다는 점을 듭니다. OWASP 지침도 같은 방향입니다. 위험한 출력 지점인 innerHTML 대신 textContent나 value를 쓰도록 코드를 고치라고 안내합니다.
그렇다면 왜 이 글에서는 변환 함수를 만들었을까요. 목록 카드처럼 제목에 <strong> 같은 내 서식을 섞은 문자열을 한 번에 조립해서 넣는 화면에서는, 조각마다 textContent를 쓰기보다 문자열을 만들면서 값만 변환하는 쪽이 코드가 짧습니다. 화면 구조가 단순하고 요소를 하나씩 만들어 붙일 수 있다면 textContent가 더 안전하고 간단합니다. 둘은 경쟁 관계가 아니라 화면을 만드는 방식에 따라 고르는 것입니다.
글자를 바꿀 때 공식 지침의 대응표
OWASP 지침은 HTML 본문에 신뢰할 수 없는 값을 넣을 때 HTML 엔티티 인코딩을 쓰라고 합니다. 아래는 이 글의 escapeHTML이 바꾸는 다섯 글자와 지침의 대응입니다.
| 원래 글자 | 바꾼 표시 코드 | 왜 바꾸나 |
|---|---|---|
| & | & | 다른 코드의 시작 글자로 읽히지 않게 합니다. 가장 먼저 바꿉니다. |
| < | < | 태그의 시작으로 읽히지 않게 합니다. |
| > | > | 태그의 끝으로 읽히지 않게 합니다. |
| " | " | 속성 값을 닫아 버리는 데 쓰이지 않게 합니다. |
| ' | ' 또는 ' | 작은따옴표로 속성 값을 닫는 데 쓰이지 않게 합니다. OWASP는 ' 표기를 예로 듭니다. |
이 글의 함수는 작은따옴표를 '로 바꿉니다. 표기만 다를 뿐 같은 글자를 가리키는 숫자 코드입니다.
링크 주소에 대한 공식 권고
OWASP는 신뢰할 수 없는 주소를 href나 src에 넣을 때의 방어로 http와 https 주소만 허용하는 방식을 안내합니다. 자바스크립트 프로토콜은 피하라는 말도 함께 있습니다. 이 글의 safeURL이 http://, https://, /로 시작하는 값만 통과시키는 것과 같은 방향입니다. 같은 문서는 프레임워크가 javascript: 주소를 막아 주더라도 그것이 서비스별 주소 검사를 대신하지는 않는다는 점도 짚습니다.
한눈에 보는 선택표
| 넣을 값 | 권장하는 방법 | 근거 |
|---|---|---|
| 순수한 글자(제목, 설명) | textContent에 넣기 | MDN, OWASP |
| 서식 문자열 안에 끼워 넣는 글자 | 다섯 글자를 변환한 뒤 조립 | OWASP 출력 인코딩 |
| 링크 주소 | http·https·내부 경로만 허용 | OWASP 주소 항목 |
| HTML 자체를 받아야 하는 경우 | 신뢰할 수 있는 정리 단계를 거치기 | MDN Trusted Types 안내 |
| 사용자가 준 스크립트·스타일 내용 | textContent로도 해결되지 않음 | OWASP 경고 |
마지막 두 줄은 이 글의 두 함수 범위를 벗어납니다. MDN은 HTML을 받아야 할 때 입력이 변환 함수를 거치게 해서 위험한 마크업을 제거하는 방식(Trusted Types와 정리 라이브러리를 함께 쓰는 예)을 소개합니다. 이 글처럼 글자와 링크만 다루는 화면이 아니라면 그쪽 문서를 먼저 읽는 편이 안전합니다.
정리하면 방어는 두 겹으로 생각하면 쉽습니다. 첫째는 값이 들어가는 자리가 글자인지 링크인지 구분하는 것이고, 둘째는 자리에 맞는 방법을 쓰는 것입니다. 글자 자리에는 변환이나 textContent, 링크 자리에는 허용 목록을 씁니다. 두 자리를 같은 함수 하나로 처리하려 하면 빈틈이 생깁니다. 이 글에서 함수를 둘로 나눈 이유도 같습니다. 또 공식 문서들이 공통으로 강조하는 것은 출처를 믿느냐가 아니라 화면에 들어가는 지점에서 거른다는 원칙입니다. 공공기관 데이터처럼 믿을 만한 출처도 예외가 아닙니다.
적용 전 확인 순서
- 외부에서 오는 값이 어디에 들어가는지 글자, 링크, 속성, 스타일로 나눠 적습니다.
- 글자라면
textContent로 충분한지 먼저 봅니다. - 문자열을 조립해 넣어야 한다면 값마다
escapeHTML을 거칩니다. - 링크라면
safeURL을 거칩니다. - 속성이나 스타일에 넣는 값이 있으면 이 글의 범위 밖이므로 공식 지침을 따로 읽습니다.
확인 자료
- MDN: Element.innerHTML (보안 고려사항)
- MDN: Node.textContent (innerHTML과의 차이)
- OWASP: Cross Site Scripting Prevention Cheat Sheet
위 문서는 2026년 10월 기준 내용입니다. 정책과 권고는 바뀔 수 있으니 적용 전에 최신 문서를 확인하세요.