Skip to content

Fix XSS vulnerabilities in markdown links and embed titles#56

Merged
Glitchii merged 2 commits intoGlitchii:mainfrom
Sim-hu:fix/xss-markdown-links-and-innerhtml
Apr 11, 2026
Merged

Fix XSS vulnerabilities in markdown links and embed titles#56
Glitchii merged 2 commits intoGlitchii:mainfrom
Sim-hu:fix/xss-markdown-links-and-innerhtml

Conversation

@Sim-hu
Copy link
Copy Markdown
Contributor

@Sim-hu Sim-hu commented Mar 20, 2026

Summary

  • Markdown link XSS (HIGH): The regex replacing [text](url) markdown syntax injected the URL directly into href without validation. A javascript: protocol URL (e.g. [click](javascript:alert(1))) would execute arbitrary JS. The fix validates that only http:// and https:// URLs produce links, escapes quotes in the href, and HTML-encodes the link text via encodeHTML.
  • Embed title innerHTML XSS (MEDIUM): embed.title from user-provided JSON was inserted into innerHTML without escaping in two places (GUI embed name display on load and on edit). The fix applies encodeHTML() to the title/value before insertion.

Test plan

  • Enter [click me](javascript:alert(1)) in an embed description and verify it renders as plain text, not a clickable link
  • Enter [legit](https://example.com) and verify it still renders as a working link
  • Enter [test](data:text/html,<script>alert(1)</script>) and verify it renders as plain text
  • Set an embed title to <img src=x onerror=alert(1)> via JSON input and verify the HTML is escaped in the sidebar embed name
  • Edit an embed title in the GUI to <script>alert(1)</script> and verify it appears as escaped text

Summary by Sourcery

Harden markdown rendering and embed title handling against XSS injection vectors.

Bug Fixes:

  • Sanitize markdown link parsing so only http/https URLs become clickable links and link text is HTML-escaped to prevent script injection via href or link text.
  • Escape embed titles before inserting them into innerHTML in both initial render and edit flows to prevent XSS from user-controlled embed names.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 20, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Tightens markdown link rendering and embed title handling to prevent XSS by validating link URLs and HTML-encoding user-controlled text before inserting it into the DOM.

Sequence diagram for safe embed title editing

sequenceDiagram
    actor User
    participant GUI as GuiForm
    participant Script as ScriptJS
    participant DOM

    User->>GUI: Edit embed title field
    GUI->>Script: Input event with new title value
    Script->>Script: Set embedObj.title = value
    Script->>Script: safeTitle = encodeHTML(value)
    Script->>DOM: Update guiEmbedName .text innerHTML
    Note right of DOM: Uses Embed N: <span>safeTitle</span>
    Script->>Script: buildEmbed(only, index)
Loading

File-Level Changes

Change Details Files
Harden markdown link parsing to prevent XSS via unsafe href protocols and unescaped link text.
  • Replace simple regex replacement for markdown links with a callback function that receives link text and href separately.
  • Validate that the href starts with http:// or https:// (case-insensitive); otherwise render the text without a link.
  • Escape double quotes in href values to avoid breaking the attribute context.
  • HTML-encode link text for both the anchor title attribute and inner content using encodeHTML().
assets/js/script.js
Escape user-controlled embed titles before inserting into innerHTML for GUI display.
  • When building the GUI from JSON, wrap embed.title in encodeHTML() before injecting it into the embed name template.
  • When editing an embed title in the GUI, wrap the new title value in encodeHTML() before updating the corresponding .text element via innerHTML.
assets/js/script.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 security issues, 2 other issues, and left some high level feedback:

Security issues:

  • User controlled data in methods like innerHTML, outerHTML or document.write is an anti-pattern that can lead to XSS vulnerabilities (link)
  • User controlled data in a guiEmbedName.querySelector('.text').innerHTML is an anti-pattern that can lead to XSS vulnerabilities (link)
  • User controlled data in methods like innerHTML, outerHTML or document.write is an anti-pattern that can lead to XSS vulnerabilities (link)
  • User controlled data in a guiEmbedName.querySelector('.text').innerHTML is an anti-pattern that can lead to XSS vulnerabilities (link)

General comments:

  • In the markdown link replacement, safeHref only replaces double quotes; consider using the same encodeHTML (or a dedicated attribute-escaping helper) for the href value as well to robustly handle <, >, and & in URLs.
  • For the embed title rendering, you might consider setting .textContent on separate nodes instead of constructing HTML strings with innerHTML (even with encodeHTML), which would further reduce the risk of future XSS regressions when these templates are modified.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the markdown link replacement, `safeHref` only replaces double quotes; consider using the same `encodeHTML` (or a dedicated attribute-escaping helper) for the `href` value as well to robustly handle `<`, `>`, and `&` in URLs.
- For the embed title rendering, you might consider setting `.textContent` on separate nodes instead of constructing HTML strings with `innerHTML` (even with `encodeHTML`), which would further reduce the risk of future XSS regressions when these templates are modified.

## Individual Comments

### Comment 1
<location path="assets/js/script.js" line_range="459-461" />
<code_context>

             // parse text in brackets and then the URL in parentheses.
-            .replace(/\[([^\[\]]+)\]\((.+?)\)/g, `<a title="$1" target="_blank" class="anchor" href="$2">$1</a>`)
+            .replace(/\[([^\[\]]+)\]\((.+?)\)/g, (_, text, href) => {
+                if (!/^https?:\/\//i.test(href)) return text;
+                const safeHref = href.replace(/"/g, '&quot;');
+                return `<a title="${encodeHTML(text)}" target="_blank" class="anchor" href="${safeHref}">${encodeHTML(text)}</a>`;
+            })
</code_context>
<issue_to_address>
**🚨 issue (security):** Escape more than just quotes in the href attribute to avoid malformed HTML and edge-case XSS vectors.

Because `href` is interpolated directly into an attribute, escaping only `"` isn’t enough. Unescaped characters like `&`, `<`, and `>` can still break the attribute or be used for XSS. Instead of `href.replace(/"/g, '&quot;')`, pass `href` through `encodeHTML` (or a dedicated attribute-escaping helper), e.g.:

```js
const safeHref = encodeHTML(href);
return `<a title="${encodeHTML(text)}" target="_blank" class="anchor" href="${safeHref}">${encodeHTML(text)}</a>`;
```

If you need to keep raw `&` in query strings, consider a URL/attribute-specific escaper, but avoid relying solely on quote replacement.
</issue_to_address>

### Comment 2
<location path="assets/js/script.js" line_range="458-463" />
<code_context>
             // parse text in brackets and then the URL in parentheses.
-            .replace(/\[([^\[\]]+)\]\((.+?)\)/g, `<a title="$1" target="_blank" class="anchor" href="$2">$1</a>`)
+            .replace(/\[([^\[\]]+)\]\((.+?)\)/g, (_, text, href) => {
+                if (!/^https?:\/\//i.test(href)) return text;
+                const safeHref = href.replace(/"/g, '&quot;');
+                return `<a title="${encodeHTML(text)}" target="_blank" class="anchor" href="${safeHref}">${encodeHTML(text)}</a>`;
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Consider HTML-encoding `text` even when you skip link creation.

Right now the non-`http(s)` branch returns `text` raw, while the `http(s)` branch HTML-encodes it. If `text` can contain `<`, `&`, etc., this can lead to HTML injection or at least inconsistent rendering. Consider:

```js
if (!/^https?:\/\//i.test(href)) return encodeHTML(text);
```

so all paths encode `text` and don’t rely on upstream sanitization.

```suggestion
            // parse text in brackets and then the URL in parentheses.
            .replace(/\[([^\[\]]+)\]\((.+?)\)/g, (_, text, href) => {
                if (!/^https?:\/\//i.test(href)) return encodeHTML(text);
                const safeHref = href.replace(/"/g, '&quot;');
                return `<a title="${encodeHTML(text)}" target="_blank" class="anchor" href="${safeHref}">${encodeHTML(text)}</a>`;
            })
```
</issue_to_address>

### Comment 3
<location path="assets/js/script.js" line_range="593" />
<code_context>
                    guiEmbedName.querySelector('.text').innerHTML = `Embed ${i + 1}${embed.title ? `: <span>${encodeHTML(embed.title)}</span>` : ''}`;
</code_context>
<issue_to_address>
**security (javascript.browser.security.insecure-document-method):** User controlled data in methods like `innerHTML`, `outerHTML` or `document.write` is an anti-pattern that can lead to XSS vulnerabilities

*Source: opengrep*
</issue_to_address>

### Comment 4
<location path="assets/js/script.js" line_range="593" />
<code_context>
                    guiEmbedName.querySelector('.text').innerHTML = `Embed ${i + 1}${embed.title ? `: <span>${encodeHTML(embed.title)}</span>` : ''}`;
</code_context>
<issue_to_address>
**security (javascript.browser.security.insecure-innerhtml):** User controlled data in a `guiEmbedName.querySelector('.text').innerHTML` is an anti-pattern that can lead to XSS vulnerabilities

*Source: opengrep*
</issue_to_address>

### Comment 5
<location path="assets/js/script.js" line_range="807" />
<code_context>
                                    guiEmbedName.querySelector('.text').innerHTML = `${guiEmbedName.innerText.split(':')[0]}${value ? `: <span>${encodeHTML(value)}</span>` : ''}`;
</code_context>
<issue_to_address>
**security (javascript.browser.security.insecure-document-method):** User controlled data in methods like `innerHTML`, `outerHTML` or `document.write` is an anti-pattern that can lead to XSS vulnerabilities

*Source: opengrep*
</issue_to_address>

### Comment 6
<location path="assets/js/script.js" line_range="807" />
<code_context>
                                    guiEmbedName.querySelector('.text').innerHTML = `${guiEmbedName.innerText.split(':')[0]}${value ? `: <span>${encodeHTML(value)}</span>` : ''}`;
</code_context>
<issue_to_address>
**security (javascript.browser.security.insecure-innerhtml):** User controlled data in a `guiEmbedName.querySelector('.text').innerHTML` is an anti-pattern that can lead to XSS vulnerabilities

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread assets/js/script.js Outdated
Comment thread assets/js/script.js
Comment thread assets/js/script.js
Comment thread assets/js/script.js
Comment thread assets/js/script.js
Comment thread assets/js/script.js
@Glitchii Glitchii merged commit 08b064d into Glitchii:main Apr 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants