CVE-2026-27469
Description
Isso is a lightweight commenting server written in Python and JavaScript. In commits before 0afbfe0691ee237963e8fb0b2ee01c9e55ca2144, there is a stored Cross-Site Scripting (XSS) vulnerability affecting the website and author comment fields. The website field was HTML-escaped using quote=False, which left single and double quotes unescaped. Since the frontend inserts the website value directly into a single-quoted href attribute via string concatenation, a single quote in the URL breaks out of the attribute context, allowing injection of arbitrary event handlers (e.g. onmouseover, onclick). The same escaping is missing entirely from the user-facing comment edit endpoint (PUT /id/) and the moderation edit endpoint (POST /id//edit/). This issue has been patched in commit 0afbfe0691ee237963e8fb0b2ee01c9e55ca2144. To workaround, nabling comment moderation (moderation = enabled = true in isso.cfg) prevents unauthenticated users from publishing comments, raising the bar for exploitation, but it does not fully mitigate the issue since a moderator activating a malicious comment would still expose visitors.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
issoPyPI | < 0.13.2 | 0.13.2 |
Affected products
1Patches
10afbfe0691eeFix stored XSS in website and author fields
2 files changed · +91 −1
isso/tests/test_comments.py+65 −0 modified@@ -69,6 +69,22 @@ def testCreate(self): self.assertEqual(rv["mode"], 1) self.assertEqual(rv["text"], '<p>Lorem ipsum ...</p>') + def testWebsiteXSSPayloadIsEscaped(self): + """Website field with XSS payload must have quotes HTML-escaped.""" + payload = "http://x.com/?'onmouseover='alert(document.domain)'x='" + rv = self.post('/new?uri=%2Fpath%2F', + data=json.dumps({'text': 'Hello', 'website': payload})) + self.assertEqual(rv.status_code, 201) + rv = loads(rv.data) + # Single quotes must be HTML-escaped so they cannot break out of an + # HTML attribute context (e.g. href='...') + self.assertNotIn("'", rv["website"]) + self.assertNotIn('"', rv["website"]) + self.assertEqual( + rv["website"], + "http://x.com/?'onmouseover='alert(document.domain)'x='", + ) + def textCreateWithNonAsciiText(self): rv = self.post('/new?uri=%2Fpath%2F', @@ -391,6 +407,27 @@ def testUpdate(self): self.assertEqual(rv['website'], 'http://example.com/') self.assertIn('modified', rv) + def testUpdateWebsiteXSSPayloadIsEscaped(self): + """Website and author XSS payloads via edit endpoint must be HTML-escaped.""" + self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'Lorem ipsum ...'})) + + website_payload = "http://x.com/?'onmouseover='alert(document.domain)'x='" + author_payload = "<script>alert(1)</script>" + self.put('/id/1', data=json.dumps({ + 'text': 'Hello World', + 'author': author_payload, + 'website': website_payload, + })) + + rv = loads(self.get('/id/1?plain=1').data) + self.assertNotIn("'", rv["website"]) + self.assertEqual( + rv["website"], + "http://x.com/?'onmouseover='alert(document.domain)'x='", + ) + self.assertNotIn("<script>", rv["author"]) + self.assertEqual(rv["author"], "<script>alert(1)</script>") + def testUpdateForbidden(self): self.post('/new?uri=test', data=json.dumps({'text': 'Hello world!'})) @@ -860,6 +897,34 @@ def testModerateComment(self): # Comment should no longer exist self.assertEqual(self.app.db.comments.get(id_), None) + def testModerateEditXSSPayloadIsEscaped(self): + """XSS payloads in author/website via moderate edit endpoint must be HTML-escaped.""" + id_ = 1 + signed = self.app.sign(id_) + + self.client.post('/new?uri=/moderated', data=json.dumps({"text": "..."})) + + website_payload = "http://x.com/?'onmouseover='alert(document.domain)'x='" + author_payload = "<script>alert(1)</script>" + rv = self.client.post( + '/id/%d/edit/%s' % (id_, signed), + data=json.dumps({ + "text": "new text", + "author": author_payload, + "website": website_payload, + }), + ) + self.assertEqual(rv.status_code, 200) + + stored = self.app.db.comments.get(id_) + self.assertNotIn("'", stored["website"]) + self.assertEqual( + stored["website"], + "http://x.com/?'onmouseover='alert(document.domain)'x='", + ) + self.assertNotIn("<script>", stored["author"]) + self.assertEqual(stored["author"], "<script>alert(1)</script>") + class TestUnsubscribe(unittest.TestCase):
isso/views/comments.py+26 −1 modified@@ -335,10 +335,13 @@ def new(self, environ, request, uri): if not valid: return BadRequest(reason) - for field in ("author", "email", "website"): + for field in ("author", "email"): if data.get(field) is not None: data[field] = escape(data[field], quote=False) + if data.get("website") is not None: + data["website"] = escape(data["website"], quote=True) + if data.get("website"): data["website"] = normalize(data["website"]) @@ -548,6 +551,13 @@ def edit(self, environ, request, id): if not valid: return BadRequest(reason) + for field in ("author",): + if data.get(field) is not None: + data[field] = escape(data[field], quote=False) + + if data.get("website") is not None: + data["website"] = escape(data["website"], quote=True) + data['modified'] = time.time() with self.isso.lock: @@ -794,6 +804,21 @@ def moderate(self, environ, request, id, action, key): return Response("Comment has been activated", 200) elif action == "edit": data = request.json + + for key in set(data.keys()) - set(["text", "author", "website"]): + data.pop(key) + + valid, reason = API.verify(data) + if not valid: + return BadRequest(reason) + + for field in ("author",): + if data.get(field) is not None: + data[field] = escape(data[field], quote=False) + + if data.get("website") is not None: + data["website"] = escape(data["website"], quote=True) + with self.isso.lock: rv = self.comments.update(id, data) for key in set(rv.keys()) - API.FIELDS:
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-9fww-8cpr-q66rghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27469ghsaADVISORY
- docs.python.org/3/library/html.htmlnvdWEB
- github.com/isso-comments/isso/commit/0afbfe0691ee237963e8fb0b2ee01c9e55ca2144nvdWEB
- github.com/isso-comments/isso/security/advisories/GHSA-9fww-8cpr-q66rnvdWEB
News mentions
0No linked articles in our index yet.