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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

critical

How stored XSS happens in TinyMCE plugins and how to fix it

The snippets plugin's `Main.ts` inserted raw, unsanitized snippet content directly into the TinyMCE editor via `editor.insertContent(snippet.content)`, allowing stored JavaScript payloads saved by any snippet editor to execute in every user's browser. The fix routes snippet content through TinyMCE's own parser and serializer before insertion, stripping dangerous markup while preserving legitimate formatting.

critical

How CSS Injection via Weak Pattern Validation happens in Vue.js and how to fix it

A critical CSS injection vulnerability in `testpage/App.vue` allowed attackers to bypass weak HTML5 pattern validation and load malicious stylesheets. The fix replaces direct variable assignment with a hardened `setCustomStylesheetHref()` method using strict regex validation.

high

How SQL-injection-style template literal injection happens in JavaScript DOM rendering and how to fix it

A Semgrep rule (`utils.custom.sql-injection-template-literal`) flagged `src/export/SheetMusicView.js` for building a query/markup string out of a JavaScript template literal with untrusted values interpolated directly into it. In this case the sink was an `<option value="${s.id}">${s.name}</option>` string used to build the snippet picker, meaning any snippet name containing `"` or `<` could break out of the attribute and inject arbitrary HTML. The fix introduces an `_escapeHtml()` helper and ro

critical

How Unvalidated External Data Fetch happens in React and how to fix it

The Datasets.jsx component fetched a remote manifest from snapshots.qdrant.io and rendered its contents directly into React state without validating response status, JSON shape, or field types. A compromised or spoofed endpoint could have injected malicious payloads straight into the UI; the fix adds strict validation and type coercion before the data ever reaches the render tree.

medium

How Cross-Site Scripting Happens in JavaScript Parsers and How to Fix It

A cross-site scripting vulnerability in JSXGraph's JessieCode parser allowed attackers to inject JavaScript through maliciously crafted input that appeared in error messages. The fix ensures proper output encoding when user-controlled data is included in parser error reporting.

critical

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

A critical XSS vulnerability was discovered in the `sanitizeInput()` function in script.js, where only angle brackets were being escaped while quotes, ampersands, and backticks remained unprotected. This incomplete sanitization allowed attackers to craft payloads using event handlers and template literals that bypassed the security controls entirely. The fix implements comprehensive HTML entity encoding for all XSS-relevant characters.