Back to Blog
critical SEVERITY8 min read

How Cross-Site Scripting (XSS) happens in JavaScript template rendering and how to fix it

A cross-site scripting (XSS) vulnerability in `renderer/views/library.js` allowed attackers who could control mod metadata—such as category icons rendered in pack thumbnail grids—to inject arbitrary JavaScript through unescaped output in `innerHTML` assignments. The fix wraps the `catIcon()` return value in the existing `esc()` helper, ensuring all dynamically generated HTML content is properly encoded before insertion into the DOM.

O
By Orbis AppSec
Published August 10, 2026Reviewed August 10, 2026

Answer Summary

This is a Cross-Site Scripting (XSS) vulnerability (CWE-79) in the JavaScript file `renderer/views/library.js`, specifically in the `packThumbGridHtml()` function at line 56. The root cause is that the return value of `catIcon(m.categoryId)` was interpolated directly into an `innerHTML` template string without HTML encoding, while other values in the same function were already protected by the `esc()` helper. The fix is a single-character change: wrapping `catIcon(m.categoryId)` with `esc()` so all dynamic content in that template literal is HTML-encoded before DOM insertion.

Vulnerability at a Glance

cweCWE-79
fixWrap `catIcon(m.categoryId)` with the existing `esc()` helper in `packThumbGridHtml()`
riskAttacker-controlled mod metadata executes arbitrary JavaScript in the application renderer
languageJavaScript
root cause`catIcon(m.categoryId)` interpolated into `innerHTML` without HTML encoding, unlike surrounding values
vulnerabilityCross-Site Scripting (XSS)

How Cross-Site Scripting (XSS) Happens in JavaScript Template Rendering and How to Fix It


The Vulnerability at a Glance

Field Detail
Vulnerability Cross-Site Scripting (XSS)
CWE CWE-79
Language JavaScript
Risk Attacker-controlled mod metadata executes arbitrary JavaScript in the application renderer
Root Cause catIcon(m.categoryId) interpolated into innerHTML without HTML encoding
Fix Wrap catIcon(m.categoryId) with the existing esc() helper

Direct Answer

This is a Cross-Site Scripting (XSS) vulnerability (CWE-79) in renderer/views/library.js, specifically in the packThumbGridHtml() function at line 56. The return value of catIcon(m.categoryId) was interpolated directly into an innerHTML template string without HTML encoding, while other dynamic values in the same template (like p, the preview URL) were already protected by the esc() helper. The fix wraps catIcon(m.categoryId) with esc(), ensuring all dynamic content in that template literal is HTML-encoded before DOM insertion.


Introduction

The renderer/views/library.js file is responsible for rendering the visual library interface of a Tauri-based desktop application—displaying mod packs, their thumbnails, and associated metadata. Most of the dynamic values flowing into its innerHTML assignments were already being sanitized through an esc() helper function. But one value slipped through: the return value of catIcon(m.categoryId) inside packThumbGridHtml().

This inconsistency—a single unescaped interpolation surrounded by properly escaped ones—is exactly the kind of subtle mistake that creates real XSS exposure. The surrounding code looked safe, which makes this easy to miss in code review.


The Vulnerability Explained

What Was Happening at Line 56

Inside packThumbGridHtml(), the code builds HTML for a pack thumbnail grid by iterating over member records. For members without a preview image, it renders a fallback cell using a category icon:

// BEFORE (vulnerable)
if (!p) return `<div class="pack-thumb-cell"><span class="ms">${catIcon(m.categoryId)}</span></div>`;

Compare this to the lines immediately below it, where preview URLs are properly escaped:

return isVideo(p)
  ? `<video src="${esc(p)}" muted playsinline preload="metadata"></video>`
  : `<img src="${esc(p)}" loading="lazy" alt="">`;

The esc() function is called for p in both the <video> and <img> cases. But catIcon(m.categoryId) is inserted raw into the <span> content—no esc() call, no encoding.

Why This Is Exploitable

The categoryId field comes from mod metadata—data that can originate from malicious preset files or mod packs loaded by the application. If an attacker crafts a mod pack where a member's categoryId maps to a category icon string containing HTML, that string lands unescaped in the DOM.

For example, if catIcon() returns a value derived from or influenced by m.categoryId, and an attacker supplies:

"><img src=x onerror=alert(1)>

...the resulting HTML becomes:

<div class="pack-thumb-cell">
  <span class="ms">"><img src=x onerror=alert(1)></span>
</div>

The injected onerror handler executes JavaScript in the renderer process. In a Tauri application, the renderer runs in an Electron-like WebView context—script execution here can have access to Tauri's IPC bridge, potentially escalating to system-level operations depending on the application's permission model.

The Broader Pattern Risk

The PR description notes that library.js uses innerHTML with string concatenation in multiple locations for rendering mod lists, pack rows, and member rows. The esc() function only escapes &, <, >, and ". While this covers standard HTML injection in element content and quoted attribute values, the pattern itself is inherently fragile: every new template literal added to the file is a potential XSS if a developer forgets to wrap a value in esc().


The Fix

What Changed

The fix is surgical and minimal—a single esc() call wrapping the previously unescaped catIcon() output:

// BEFORE (vulnerable) — renderer/views/library.js:56
if (!p) return `<div class="pack-thumb-cell"><span class="ms">${catIcon(m.categoryId)}</span></div>`;

// AFTER (fixed)
if (!p) return `<div class="pack-thumb-cell"><span class="ms">${esc(catIcon(m.categoryId))}</span></div>`;

Why This Works

The esc() function converts HTML-special characters into their entity equivalents before they reach the DOM parser. So if catIcon(m.categoryId) returns anything containing <, >, ", or &, those characters are neutralized:

Input character Encoded output
< &lt;
> &gt;
" &quot;
& &amp;

The browser renders these as literal characters in the text content of the <span>, rather than interpreting them as HTML markup. The injected onerror handler never gets parsed.

Behavior Preservation

For valid category icon values—which are expected to be simple strings like "extension" or "auto_awesome" (Material Symbols icon names)—esc() is a no-op. No visual change occurs for legitimate data. Only malicious or malformed input is affected.


Prevention & Best Practices

1. Apply esc() Consistently to Every Dynamic Value in innerHTML

The root cause here was inconsistency: some values were escaped, one was not. A code review checklist item for this codebase should be: "Does every ${} interpolation inside an innerHTML template literal pass through esc()?"

Consider a linting rule or ESLint plugin (e.g., eslint-plugin-no-unsanitized) that flags raw interpolations into innerHTML.

2. Prefer DOM APIs Over innerHTML for Dynamic Content

Where feasible, replace innerHTML template construction with explicit DOM API calls:

// Safer alternative
const span = document.createElement('span');
span.className = 'ms';
span.textContent = catIcon(m.categoryId); // textContent never parses HTML
const div = document.createElement('div');
div.className = 'pack-thumb-cell';
div.appendChild(span);
return div.outerHTML;

textContent treats its value as a text node—no HTML parsing occurs, so no encoding is needed.

3. Consider a Sanitization Library for Complex HTML

If the application needs to render richer HTML from external sources, use DOMPurify rather than a hand-rolled esc() function. DOMPurify handles a broader range of XSS vectors including SVG-based injection, namespace confusion attacks, and more.

4. Audit All innerHTML Assignments in the File

The PR description notes that library.js uses this pattern in multiple locations. A full audit should verify that every innerHTML assignment in the file—for mod lists, pack rows, and member rows—applies esc() to every interpolated value.

5. Treat Mod Metadata as Untrusted Input

Even locally-loaded files (preset files, mod packs) should be treated as potentially attacker-controlled. Users may share packs, download them from third-party sources, or be socially engineered into loading malicious files. The threat model should assume any field in a mod record can contain adversarial content.

OWASP & Standards Alignment

  • OWASP XSS Prevention Cheat Sheet: Recommends output encoding as the primary defense, with the encoding function matched to the output context.
  • CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

Key Takeaways

  • catIcon(m.categoryId) was the only unescaped interpolation in a template literal where every other dynamic value was wrapped in esc()—a subtle inconsistency that created real XSS exposure.
  • Mod metadata is attacker-controlled input. Fields like categoryId originate from files that users load from external sources; they must be treated with the same skepticism as HTTP request parameters.
  • The esc() helper was already present and working—this vulnerability existed not because the tool was missing, but because it wasn't applied uniformly. Consistency in applying sanitization is as important as having the sanitization function.
  • In Tauri renderer contexts, XSS has elevated risk. Script execution in the WebView may have access to Tauri IPC, making renderer-level XSS a potential stepping stone to system-level impact.
  • A one-line fix closed the vector: wrapping catIcon(m.categoryId) with esc() in packThumbGridHtml() is all that was needed, with zero impact on valid inputs.

How Orbis AppSec Detected This

  • Source: The m.categoryId field from a member record in a mod pack—data that originates from externally loaded preset or pack files and is passed to catIcon().
  • Sink: The unencoded ${catIcon(m.categoryId)} interpolation inside an innerHTML template literal in packThumbGridHtml() at renderer/views/library.js:56.
  • Missing control: No call to esc() (or any other HTML encoding function) around the catIcon() return value before DOM insertion, despite esc() being applied to all other dynamic values in the same template.
  • CWE: CWE-79 — Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
  • Fix: Wrapped catIcon(m.categoryId) with the existing esc() helper to HTML-encode the value before it is inserted into the DOM via innerHTML.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

This vulnerability is a textbook example of how XSS hides in plain sight: the packThumbGridHtml() function in library.js was mostly doing the right thing. Preview URLs were escaped. Video sources were escaped. But one value—the category icon string returned by catIcon(m.categoryId)—was not, and that single omission was enough to open an injection path for anyone who could influence mod metadata.

The fix is a one-line change that brings catIcon(m.categoryId) in line with the encoding discipline already applied to the surrounding code. But the broader lesson is about consistency: security controls are only as strong as their most inconsistent application. If your codebase uses innerHTML with template literals, every single interpolated value needs to go through your encoding function—no exceptions, no assumptions about what "safe" values look like.

Automated tools that trace data flow from sources (external files, user input) to sinks (innerHTML, eval, document.write) are the most reliable way to catch these inconsistencies before they reach production.


References

Frequently Asked Questions

What is Cross-Site Scripting (XSS)?

XSS is an injection vulnerability where attacker-controlled data is rendered as executable HTML or JavaScript in a victim's browser or renderer context, allowing script execution, data theft, or UI manipulation.

How do you prevent XSS in JavaScript innerHTML assignments?

Always encode every dynamic value inserted into an innerHTML string using a trusted HTML-escaping function (like an `esc()` helper or `DOMPurify`), or better yet, use `textContent`, `createElement`, or a framework's safe templating API instead of raw `innerHTML`.

What CWE is Cross-Site Scripting?

XSS maps to CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

Is escaping `&`, `<`, `>`, and `"` enough to prevent XSS?

For most HTML contexts it covers the critical cases, but it may be insufficient for attribute contexts (unquoted attributes), JavaScript contexts, or CSS contexts. Always ensure the escaping function matches the output context.

Can static analysis detect this type of XSS?

Yes. Tools like Semgrep, ESLint security plugins, and AI-assisted scanners can trace unescaped values flowing into `innerHTML` assignments and flag them as potential XSS sinks.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

critical

How Cross-Site Scripting (XSS) happens in JavaScript innerHTML and how to fix it

A critical Cross-Site Scripting (XSS) vulnerability was discovered in `js/main.js` where commit messages fetched from the GitHub API were directly interpolated into `innerHTML` without any sanitization. An attacker with repository write access could push a commit with a malicious message like `<img src=x onerror=alert(document.cookie)>`, causing arbitrary JavaScript execution in every visitor's browser. The fix applies HTML entity encoding to all five dangerous characters before rendering.

critical

How XSS via unescaped sender_name happens in JavaScript chat widgets and how to fix it

A stored Cross-Site Scripting (XSS) vulnerability in `frontend/scripts/chat-widget.js` allowed attackers to inject arbitrary JavaScript by crafting a malicious `sender_name` field, which was interpolated directly into a DOM template string without HTML encoding. The `renderFileContent()` function compounded the risk by also inserting unsanitized `file.name` and `file.url` values into `img`, `span`, and `anchor` elements. The fix applies `AppUtils.escapeHTML()` to every user-controlled value befo

critical

How HTML sanitizer bypass via XMP tag passthrough happens in Node.js and how to fix it

A critical cross-site scripting (XSS) vulnerability in the sanitize-html library allowed attackers to bypass HTML sanitization through improper handling of the `<xmp>` raw-text element. This vulnerability could enable stored XSS attacks in any Node.js application using sanitize-html versions prior to 2.17.4. The fix involved upgrading the library to properly handle raw-text elements and prevent script injection.

critical

How DOM-based XSS via jQuery .html() happens in JavaScript and how to fix it

A critical DOM-based Cross-Site Scripting (XSS) vulnerability was discovered in CustomRankingInterface.js where user-imported JSON data containing malicious filter names could execute arbitrary JavaScript in victims' browsers. The fix replaces jQuery's unsafe `.html()` method with the safe `.text()` method, preventing script injection while preserving the intended functionality.

critical

How reflected XSS happens in Jinja2 template rendering and how to fix it

A reflected cross-site scripting (XSS) vulnerability was discovered in the similarity search HTML template where user input from the `query` form parameter was rendered directly into an HTML attribute without proper escaping. An attacker could inject malicious JavaScript by crafting a search query containing attribute-breaking payloads like `" onfocus="alert(document.cookie)" autofocus="`, which would execute in the victim's browser.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.