본문 바로가기
빌드노트 검색

빌드노트

innerHTML 문자열 대입이 CSP 적용 뒤 TypeError로 멈춘다

CSP 적용 뒤 innerHTML 문자열 대입이 TypeError로 멈추는 조건을 확인한다. Chromium에서 정책 유무와 textContent 수정의 DOM·오류를 비교하고, 평문 출력과 정화된 HTML 경로를 선택하는 기준을 설명한다.

innerHTML에 안내 문구를 넣던 코드가 CSP 적용 뒤 TypeError를 내고, 화면에는 기존 내용만 남는다면 require-trusted-types-for 'script'가 적용됐는지 확인한다. 이 정책을 강제하고 기본 정책이 없는 환경에서는 HTML 입력을 받는 innerHTML에 일반 문자열을 그대로 넣을 수 없다. 평문을 표시할 자리라면 해당 대입을 textContent로 바꾸고, 서식 있는 HTML이 필요하다면 정화와 TrustedHTML 생성 경로를 확인한다.

오류가 난 줄과 HTML 응답의 정책을 맞춘다

먼저 개발자 도구의 Console에서 오류가 난 파일과 줄을 연다. 다음은 별도 실습의 Chromium에서 innerHTML에 문자열을 넣었을 때 잡은 오류다. 이 메시지가 자신의 오류와 같은 종류인지 확인할 수 있다.

TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.

그다음 Network를 연 상태로 문제 페이지를 다시 불러온다. 현재 HTML 문서 요청의 Response Headers에서 Content-Security-Policy에 require-trusted-types-for 'script'가 있는지 본다. JS 파일의 응답 헤더만 보고 판단하지 않는다. HTML의 head에 CSP를 전달하는 meta가 있는지도 확인한다. MDN의 CSP 전달 방식 (새 탭에서 열림)은 응답 헤더와 meta를 구분해 설명한다. iframe 안에서 실패했다면 해당 프레임의 문서를 대상으로 확인한다.

MDN의 require-trusted-types-for 설명 (새 탭에서 열림)은 innerHTML 같은 DOM XSS 입력 지점에도 이 지시어가 적용된다고 설명한다. 여기서 'script'는 검사 대상 그룹의 이름이다. HTML 문자열에 script 태그가 들어 있어야만 검사한다는 뜻으로 읽으면 안 된다.

헤더 이름이 Content-Security-Policy-Report-Only라면 그 정책은 위반을 보고할 뿐 대입을 차단하지 않는다. 강제 정책이 함께 내려오는지도 확인한다. 또 innerHTML의 예외 조건 (새 탭에서 열림)에는 기본 정책이 없는 경우가 명시돼 있다. 기존 앱에 default 정책이 있으면 브라우저가 그 변환을 자동 호출할 수 있으므로, 문자열 대입의 동작이 아래 실험과 다를 수 있다. 기본 정책은 Console에서 window.trustedTypes?.defaultPolicy로 조회할 수 있다.

무해한 문자열도 대입 단계에서 거절됐다

Windows에서 새 loopback 서버와 별도 headless Chromium 147.0.7727.15 세션을 열어 비교했다. 실제 서비스 장애 기록과는 구분되는 실험이다. 출력 대상은 처음에 기존 내용이라는 글자만 가진 div였다. 정책 없는 문서와 정책 있는 문서에서 같은 코드를 실행했고, Trusted Types 정책은 만들지 않았다. 강제 정책 문서의 응답 헤더 값은 정확히 require-trusted-types-for 'script'였다.

  • 정책 없이 innerHTML에 <strong>안내</strong> 대입: 오류 없이 strong 요소 하나가 생겼고, 화면의 글자는 안내로 바뀌었다.
  • 같은 문자열을 강제 정책 아래에서 innerHTML에 대입: 위 TypeError가 발생했고, DOM은 기존 내용으로 남았다.
  • 태그를 뺀 안내를 같은 정책 아래에서 innerHTML에 대입: 역시 TypeError가 발생했다.
  • 같은 정책을 유지하고 <strong>안내</strong>을 textContent에 대입: 오류 없이 꺾쇠까지 포함한 원문이 표시됐고, 자식 요소 수는 0이었다.

차단된 두 경우에는 CSP 위반 이벤트의 effectiveDirective가 require-trusted-types-for, disposition이 enforce였다. 성공한 두 경우에는 해당 위반 이벤트가 없었다. 모든 문서에서 Trusted Types API는 존재했고 기본 정책은 null이었다. API가 있다는 사실만으로 강제 적용 여부를 알 수는 없다. 정책 없는 문서에서도 같은 API가 보였기 때문이다.

태그 없는 글자도 거절됐다는 관찰은 이 오류를 이해하는 데 도움이 된다. 브라우저가 요구하는 입력 타입이 바뀐 것이다. 문자열에서 특정 태그를 제거한 뒤 다시 innerHTML에 넣어도 여전히 문자열이므로 같은 조건에서 실패한다.

평문 표시 코드는 출력할 요소에서 바꾼다

자신의 코드에서 검색어·파일명·안내 문구를 표시하는 대입이라면 textContent로 바꾼다. 아래는 이미 DOM에 있는 #message라는 일반 div의 내용을 바꾸고, 태그가 생성되지 않았는지 확인하는 입력이다. 개발용 페이지에서 DOM 생성 뒤 실행하며, 선택자는 실제 평문 출력 요소의 ID로 바꾼다.

const message = document.querySelector('#message');
message.textContent = '<strong>안내</strong>';
console.log(JSON.stringify([message.childElementCount, message.textContent]));

이 입력의 출력은 다음과 같다. 화면에도 <strong>안내</strong>이 글자 그대로 보인다.

[0,"<strong>안내</strong>"]

message가 null이라면 선택자나 실행 시점을 먼저 고쳐야 한다. TrustedHTML을 요구하는 오류와는 다른 문제다. 또 textContent의 설정 동작 (새 탭에서 열림)은 기존 자식 노드 전체를 텍스트로 교체한다. 아이콘과 글자가 함께 있는 버튼이라면 글자용 span처럼 교체할 부분을 선택해야 아이콘까지 지우는 수정을 피할 수 있다. 여기서 다루는 대상은 화면에 평문을 표시하는 일반 요소다.

굵게 표시하거나 링크를 유지해야 하는 HTML이라면 textContent로 바꾸면 서식도 글자가 된다. 이 경우에는 앱에서 허용할 태그·속성·URL을 다루는 검증된 정화 경로와, 그 결과를 TrustedHTML로 만드는 정책을 함께 검토한다. MDN의 trusted-types 지시어 설명 (새 탭에서 열림)에 따르면 trusted-types는 생성 가능한 정책 이름을 제한하는 별도 지시어다. 정책을 추가할 때는 이 허용 목록과도 맞아야 한다.

trustedTypes.createPolicy()를 호출하는 것만으로 HTML이 정화되지는 않는다. 실제 변환 규칙은 앱이 제공한다. 입력을 그대로 돌려주는 default 정책을 추가해 오류만 없애면 검토하지 않은 문자열도 자동으로 통과할 수 있다. Trusted Types 편집 초안의 기본 정책 설명 (새 탭에서 열림)도 느슨한 기본 정책이 보호 효과를 무너뜨릴 수 있다고 설명한다. 이 문서는 확인 당시 2026년 10월 7일자 작업 중 초안이다.

이번 비교는 고정된 무해 문자열과 일반 div에서 문자열 거절 및 평문 수정의 동작을 확인한 것이다. 특정 정화 라이브러리의 안전성, 서식 있는 HTML의 TrustedHTML 연동, 다른 브라우저나 운영 앱 전체의 호환성은 검증하지 않았다. 수정 후에도 오류가 남는다면 Console이 가리키는 다음 대입 위치를 확인한다. 처음 고친 코드 외에 라이브러리나 다른 출력 경로가 innerHTML을 호출하는지는 별도로 살펴야 한다.

← 전체 글 보기