FEInterview Prep

Velog

굿바이 innerHTML, 반가워 setHTML: Firefox 148에서 한층 강화된 XSS 방어

Firefox 148이 표준 Sanitizer API와 Element.setHTML()을 최초 탑재했다. innerHTML 대입을 setHTML()로 바꾸는 최소한의 수정만으로 XSS를 기본 안전으로 끌어올릴 수 있다.

2026-04-21·6분 읽기
브라우저JavaScript
원문 보기 ↗

핵심 요약

Sanitizer API 는 신뢰할 수 없는 HTML 을 DOM 에 넣기 전에 정제하는 표준이고, Element.setHTML() 은 이 정제 단계를 삽입 API 내부에 엮어 safety by default 를 보장한다. 기본 설정이 지나치게 엄격할 때는 SanitizerConfig 로 허용 태그/속성을 직접 정의하는 커스텀 설정을 쓸 수 있다. Trusted Types 와 결합하면 require-trusted-types-for 'script' 지시어로 innerHTML 같은 raw sink 자체를 차단할 수 있어, 복잡한 CSP 설계 없이도 XSS 재발을 구조적으로 막는다. 핵심은 '라이브러리 호출 기반' → 'API 내장 강제' 로의 패러다임 전환이다.

이 글은 **'XSS는 CSP로 막는다'**는 2010년대의 방어 모델에서, **'innerHTML 대입 지점(sink)을 setHTML()로 대체한다'**는 2020년대 모델로 갈아타는 전환점으로 읽는다. 정책 레이어(CSP) → DOM sink 레이어(Sanitizer) → 강제 레이어(Trusted Types) 의 3단 방어를 각각 어떤 문제를 푸는 도구인지 구분하는 프레임이 핵심이다.

XSS는 10년 넘게 CWE 상위 3개에 고정된 취약점이지만, 대응 수단은 실무 도입 비용이 컸다.

  • Content-Security-Policy: 전사 재설계 필요, 인라인 스크립트/핸들러와 충돌
  • DOMPurify: 라이브러리 호출을 잊으면 그 순간 뚫림
  • Sanitizer + setHTML(): 대입 라인만 바꾸면 되는 점진적 접근

Firefox 148이 표준 Sanitizer 를 최초 탑재하면서 Chromium/Safari 도 따라올 것이 거의 확실하고, 면접에서 '왜 지금 setHTML 인가' 를 설명할 수 있어야 최신 보안 감각을 보여줄 수 있다.

학습 포인트

면접 답변으로 연결할 학습 포인트입니다.

기본 안전 vs 사후 정제

innerHTML정제와 삽입이 분리되어 있어 개발자가 sanitize 호출을 한 번만 빠뜨려도 뚫린다. setHTML() 은 정제를 삽입 API 내부로 끌어들여 '잊을 수 있는 단계'를 구조적으로 제거한다.

// 위험: sanitize 호출을 잊으면 바로 XSS
el.innerHTML = userInput;
el.innerHTML = DOMPurify.sanitize(userInput); // 매번 기억해야 함

// 안전: 정제가 API 에 내장
el.setHTML(userInput);

보안은 '누가 실수하지 않는가' 가 아니라 '실수해도 안전한가' 로 설계해야 한다.

Sanitizer APIsetHTMLsafety by defaultsink
자주 하는 오해

'DOMPurify 쓰면 되는 거 아니냐'로 끝내는 답변. 라이브러리는 호출을 잊으면 그만이지만, setHTML 은 API 자체가 정제를 강제하므로 본질이 다르다.

CSP와의 역할 분담

세 레이어는 푸는 문제가 다르고, 함께 쓸 때 효과가 극대화된다.

레이어대상역할
CSP리소스 로드/실행어떤 스크립트/도메인을 허용할지 정책
SanitizerHTML 콘텐츠DOM 에 들어가기 전 태그/속성 정제
Trusted TypesDOM sinkinnerHTML 등 raw sink 강제 차단

setHTML 도입 후 Trusted Types 를 켜면 raw innerHTML 경로까지 전면 차단되므로, 복잡한 CSP 튜닝 없이도 XSS 재발을 막을 수 있다.

CSPTrusted Typesrequire-trusted-types-fordefense in depth
자주 하는 오해

CSPSanitizer 를 같은 레이어로 묶어 '중복 아니냐' 고 생각하는 것. 둘은 방어 레이어가 다르다.

점진적 마이그레이션 경로

레거시 전체 재작성 없이 innerHTML = x 라인을 element.setHTML(x)기계적으로 치환하는 것만으로 대부분의 XSS 벡터가 닫힌다. 브라우저 지원이 부족한 환경은 feature detection 으로 fallback 을 둔다.

if ('setHTML' in Element.prototype) {
  el.setHTML(html);
} else {
  el.innerHTML = DOMPurify.sanitize(html);
}

리치텍스트 에디터처럼 기본 설정이 깨는 경우는 SanitizerConfig 로 허용 목록을 정의한다.

feature detectionDOMPurify fallbackSanitizerConfigallowlist
자주 하는 오해

브라우저 지원이 부족하다며 도입 자체를 전부 미루는 것. 지원 체크 후 fallback 을 두는 단계적 도입이 현실적이다.

읽는 순서

  1. 1이론

    MDN 의 Sanitizer 명세에서 '기본 설정' 과 '커스텀 설정(SanitizerConfig)' 섹션, 그리고 Trusted Typesrequire-trusted-types-for 지시어를 읽고 세 레이어(CSP / Sanitizer / Trusted Types) 의 역할을 한 줄씩 정리하세요.

  2. 2구현

    기존 프로젝트에서 innerHTMLgrep 으로 찾아내고, 그중 하나를 element.setHTML() 로 교체해 Firefox 148 이상에서 동작을 확인하세요. feature detection + DOMPurify fallback 도 같이 붙여보세요.

  3. 3실무

    가장 위험한 사용자 입력 경로(리뷰, 댓글, 리치텍스트) 를 목록화하고, Trusted Types Report-Only 정책을 켠 뒤 남는 raw sink 를 리포트로 수집해 마이그레이션 PR 순서를 설계해 보세요.

  4. 4설명

    팀에 'Sanitizer vs DOMPurify vs CSP vs Trusted Types' 비교표를 한 장으로 공유하고, 왜 setHTML 마이그레이션을 지금 시작해야 하는지(브라우저 표준화 흐름 + 도입 비용) 설득 논리를 만들어 보세요.

면접 연결 질문

mediumXSS 방어 수단 네 가지(문자열 이스케이프, CSP, DOMPurify, `setHTML`)의 차이를 한 줄씩 설명하고, 어떻게 조합해야 하는지 말해보세요.
힌트

[감점 답변] '다 안전하게 처리한다' 식으로 뭉뚱그리면 감점. [좋은 답변] 레이어를 구분한다.

  • 이스케이프: 문자열 레벨 방어 (< 치환)
  • CSP: 실행 정책 (리소스 허용 목록)
  • DOMPurify: 라이브러리 호출 기반 정제 — 호출 누락 시 뚫림
  • setHTML: API 내장 정제 — 잊어도 안전

조합: setHTML + Trusted Types 로 sink 를 강제하고, CSP 로 실행을 제한하는 defense in depth.

medium`innerHTML` 을 `setHTML` 로 바꾸는 마이그레이션을 어떻게 점진적으로 진행하시겠어요?
힌트

[감점 답변] '그냥 일괄 치환하면 된다' 는 현실성 감점. [좋은 답변] 우선순위를 정한다.

  1. 가장 위험한 입력 경로 부터: 사용자 작성 HTML, 외부 피드, 댓글/리뷰
  2. feature detection + DOMPurify fallback 으로 안전망
  3. 리치텍스트/에디터는 SanitizerConfig 로 허용 목록 커스텀
  4. Trusted Types 정책을 Report-Only 로 먼저 켜서 잔여 innerHTML 을 감지
  5. 감지 끝나면 Enforce 모드로 승격
hard`Sanitizer` API 와 `Trusted Types` 는 각각 어떤 문제를 해결하며, 같이 쓰면 어떤 시너지가 나나요?
힌트

[감점 답변] 둘을 동의어처럼 말하면 감점. [좋은 답변] 레이어 차이를 명확히:

  • Sanitizer = 콘텐츠 정제 (무엇이 들어갈 수 있는가)
  • Trusted Types = 정제된 값만 sink 에 들어가도록 강제하는 정책 (어디에 들어갈 수 있는가)

Sanitizer 만 쓰면 여전히 누군가 raw innerHTML 을 쓸 여지가 남는다. Trusted Types 를 켜면 require-trusted-types-for 'script' 로 그 경로 자체를 CSP 수준에서 차단한다. 둘을 결합해야 '실수로 raw innerHTML' 까지 막힌다.

자기 점검

`innerHTML` 대입과 `setHTML()` 호출의 차이를 '코드 레벨' 이 아니라 '보안 설계 원칙 레벨' 에서 설명해 보세요.
safety by defaultsink정제 강제Sanitizer
자주 하는 오해

'API 가 더 짧아졌다' 는 식의 표면 설명. 핵심은 '정제를 호출자에게 맡기지 않는다' 는 원칙 전환이다.

기본 `Sanitizer` 설정이 지나치게 엄격해서 리치텍스트 에디터가 깨질 때 어떻게 접근하시겠어요?
SanitizerConfigallowlistelements/attributes커스텀 설정
자주 하는 오해

sanitizer 를 꺼버리고 innerHTML 로 회귀하는 것. 필요한 태그/속성만 허용 목록 에 추가하는 것이 정답이다.

`CSP` 만으로 XSS 를 막을 수 없는 구체적 상황을 설명하고, `Sanitizer`/`Trusted Types` 가 어떻게 그 빈틈을 메우는지 말해보세요.
inline scriptDOM sinknoncedefense in depth
자주 하는 오해

CSP 만 완벽히 걸면 충분하다는 생각. CSP실행 정책이라 DOM 에 삽입된 HTML 자체의 위험 요소(이벤트 핸들러, javascript: URL)를 1차 차단해주지 못하는 경우가 많다.