setHTMLUnsafe와 setHTML, 사용자 HTML을 넣을 때 무엇이 다를까?

댓글의 굵은 글씨와 링크는 살리고 스크립트는 걸러 내고 싶다. innerHTML 자리에 setHTMLUnsafe와 setHTML 중 무엇을 넣어야 할까?

setHTMLUnsafe와 setHTML, 사용자 HTML을 넣을 때 무엇이 다를까?
세 줄 요약
  • innerHTML을 setHTMLUnsafe로 교체하는 것만으로 기본 정화가 생기지 않는다.
  • setHTML은 사용자 지정 허용 목록에도 XSS 위험 항목을 제거한다.
  • Firefox 158의 javascript: URL 제거는 Sanitizer를 적용한 호출의 변화다.

이름이 비슷해도 기본 동작은 다르다

댓글이나 편집기에서 받은 HTML을 화면에 넣을 때, innerHTML을 최신 API로 바꾸면 안전해질 것이라고 기대하기 쉽다. 하지만 setHTMLUnsafe 자체는 기본 정화를 수행하지 않는다. 별도의 Trusted Types 정책 등 외부 처리가 없는 호출에서 sanitizer를 생략하면, 위험한 내용을 걸러 주는 단계도 없다.

setHTMLUnsafe의 sanitizer 인자에 "default"를 명시하는 것과 인자를 아예 생략하는 것은 다른 동작이다. 사용자 제공 HTML에는 지원 브라우저의 setHTML이 우선 선택이다.

호출메서드가 적용하는 정화
setHTML(input)기본 Sanitizer를 적용하고 XSS 위험 항목을 제거한다.
setHTMLUnsafe(input)Sanitizer를 적용하지 않는다.
setHTMLUnsafe(input, { sanitizer: "default" })기본 Sanitizer를 명시적으로 적용한다.

이 차이는 변수 이름이나 HTML 문자열의 출처로 바뀌지 않는다. 서버에서 왔다는 이유만으로 사용자가 입력한 내용까지 신뢰할 수 있게 되는 것은 아니다.

서식을 보여줄지, 문자만 보여줄지 먼저 정한다

아래는 HTML 서식을 허용하되 setHTML이 없는 환경에서는 문자로 표시하는 작은 예다. target은 일반적인 본문용 div라고 가정한다.

function showComment(target, input) {
  if (typeof target.setHTML === "function") {
    target.setHTML(input);
  } else {
    target.textContent = input;
  }
}

textContent는 문자열을 HTML로 해석하지 않는다. 따라서 <b>hello</b>도 굵은 글자 대신 태그가 포함된 문자로 보인다. 위 분기는 같은 서식을 보장하는 폴리필이 아니라, 미지원 환경에서는 서식을 적용하지 않고 문자 그대로 표시하는 대체 방식이다.

애초에 댓글에 서식이 필요 없다면 일반 본문 요소에 textContent로 표시하면 된다. 반대로 모든 지원 브라우저에서 서식을 유지해야 한다면 기존의 검증된 정화 경로를 유지해야 하며, 미지원 분기를 원시 innerHTML 삽입으로 바꿔서는 안 된다.

Firefox 158에서 바뀐 것은 정화된 링크의 처리다

Firefox 158은 Sanitizer의 javascriptURLs 설정과 관련 메서드를 지원한다. 이에 따라 기본 Sanitizer를 쓰거나 removeUnsafe()를 적용한 Sanitizer를 쓰는 unsafe 계열 호출은 javascript: URL을 제거한다.

이는 setHTMLUnsafe를 sanitizer 옵션 없이 호출해도 정화가 켜진다는 뜻이 아니다. 반면 안전한 계열인 setHTML은 이런 URL을 계속 제거한다. 업데이트 후 링크의 동작이 달라졌다면 메서드 이름뿐 아니라 실제로 넘긴 Sanitizer 설정을 확인해야 한다.

일반 링크가 필요한지, 링크 클릭으로 스크립트를 실행하려는 것인지도 나눠야 한다. 사용자 입력을 안전하게 표시하려는 작업에서 위험한 URL을 되살리려고 정화를 통째로 끄면 처음 선택한 보호를 잃는다.

허용 목록과 재삽입 경로까지 이어서 본다

기본 Sanitizer 구성은 스크립트 관련 항목만 제거하지 않는다. 주석과 data-* 속성 등도 제거한다. 편집기 데이터가 사라지는 문제라면 어떤 속성을 보존해야 하는지부터 확인하고 필요한 구성만 조정한다.

setHTML은 사용자 지정 구성에서 허용하더라도 XSS 위험 요소·속성을 제거한다. setHTMLUnsafe는 안전 기준을 강제로 덧씌우는 메서드가 아니므로, 사용자 지정 구성을 적용할 때 그 구성이 무엇을 남기는지까지 검토해야 한다.

마지막 삽입 지점도 중요하다. HTML 표준의 보안 설명은 정화한 노드를 다시 문자열로 꺼내 파싱하는 과정에서 mutation XSS가 생길 수 있다고 설명한다. 한 요소에 안전하게 넣었던 결과를 innerHTML로 읽어 다른 요소의 innerHTML에 다시 넣는 식의 우회 경로를 만들지 않아야 한다.

메서드를 교체할 때는 ‘setHTML을 호출하는가’에서 끝내지 말고, 미지원 분기와 사용자 지정 설정, 최종 삽입까지 같은 보호가 이어지는지 확인해야 한다.