Back to Blog
high SEVERITY7 min read

escapeQuotes() Gap Lets File Names XSS Search Results

The desktop app's file-search results renderer built HTML strings from file names and paths using only `escapeQuotes()` and `escapeBackSlash()`, which strip quotes and backslashes but leave `<`, `>`, and `&` untouched. A file or folder named with an HTML payload such as `<img src=x onerror=alert(1)>` would execute when the matching search result was rendered, giving an attacker script execution in the app's DOM context.

O
By Orbis AppSec
•Published September 25, 2026•Reviewed September 25, 2026

Answer Summary

The affected code is first-party JavaScript in a `search(text)` function that builds display text from file-system metadata (`file.name`, `file.path`). An attacker who can name a file (e.g., `<img src=x onerror=alert(1)>.pdf`) gets that markup rendered live in the app's search results, achieving script execution in the UI's DOM context. The fix adds a chained `.replace()` call that HTML-entity-encodes `&`, `<`, `>`, `"`, and `'` before the text is pushed into the results array; no separate version is tracked since this is first-party code. This is classified under CWE-79, Improper Neutralization of Input During Web Page Generation.

Vulnerability at a Glance

cweCWE-79
fixAdded entity-encoding replace chain for &, <, >, ", ' before the text is stored in results
riskMalicious file/folder names execute as HTML/JS when displayed in search results
languageJavaScript
root causeescapeQuotes()/escapeBackSlash() only escape quotes and backslashes, not HTML markup characters
vulnerabilityCross-Site Scripting (XSS) via unescaped file names in search results

TITLE: escapeQuotes() Gap Lets File Names XSS Search Results

SEO_TITLE: escapeQuotes() XSS: File Search HTML Injection Fix

SEO_DESCRIPTION: escapeQuotes() in the file search renderer skips HTML entity encoding, letting crafted file names execute as script (CWE-79); the fix adds entity escaping.

SUMMARY: The desktop app's file-search results renderer built HTML strings from file names and paths using only escapeQuotes() and escapeBackSlash(), which strip quotes and backslashes but leave <, >, and & untouched. A file or folder named with an HTML payload such as <img src=x onerror=alert(1)> would execute when the matching search result was rendered, giving an attacker script execution in the app's DOM context.

INTRODUCTION: The file-search feature parses untrusted input in the form of on-disk file names and paths, and a flaw in that path created a stored cross-site scripting condition inside the app itself. Inside the async search(text) function, the code computes a display string with let text = file.matchPath ? file.path.replace(...) : file.name; and then pushes that raw string into a results array that is later rendered into the DOM. The two existing helper functions, escapeQuotes and escapeBackSlash, were designed to make strings safe for embedding inside quoted JavaScript/HTML attribute values — they were never meant to be an HTML sanitizer, but they were the only defense standing between a file name on disk and the DOM. Any code path that builds HTML from file-system metadata (names, paths, extended attributes) needs to treat that metadata as attacker-controlled, because on most platforms a user can name a file almost anything.

Affected Versions

Affected not applicable (first-party code)
Fixed in not applicable (first-party code)
Ecosystem not applicable
CVE / GHSA not assigned
CWE CWE-79 (Improper Neutralization of Input During Web Page Generation)

The Vulnerability Explained

The relevant part of the search() function reads:

let text = file.matchPath ? file.path.replace(new RegExp('^\s*'+pregQuote(file.mainPath)+pregQuote(p.sep)+'?'), '') : file.name;

results.push({
    icon: file.compressed ? 'folder_zip' : (file.folder ? 'folder' : ''),
    image: image,

Before the fix, text was pushed straight into the results array with no transformation beyond whatever escapeQuotes/escapeBackSlash did elsewhere in the file. escapeQuotes only escapes single/double quotes and escapeBackSlash only escapes backslashes — neither one touches <, >, &, or the characters that actually matter for HTML/script injection. Because search results are rendered into the DOM (as list entries showing the matched file or folder name), any file whose name or path contains raw markup gets interpreted by the browser/renderer rather than displayed as text.

Attack scenario: An attacker drops or renames a file to something like report<img src=x onerror=alert(document.cookie)>.pdf inside a directory the victim will search (a shared folder, a downloads directory, an extracted archive, etc.). When the victim opens the app's search UI and types a query that matches, search(text) computes file.name for the display string, pushes it unescaped, and the rendering layer inserts it into the results list. The onerror handler fires immediately, running attacker-controlled JavaScript in the same context as the rest of the application UI — a classic stored/DOM-based XSS, except the "storage" is the file system itself rather than a database.

For a desktop or Electron-style app, this is more than a nuisance: script execution inside the app's renderer can potentially reach IPC bridges, local file APIs, or other privileged surfaces depending on how the app is architected, turning a mislabeled file into a foothold for further compromise.

The Fix

The fix adds a dedicated HTML-entity-encoding step for the computed text value, immediately before it is placed into the results array:

text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');

This single line closes the gap left by escapeQuotes/escapeBackSlash by encoding all five characters that matter for HTML context injection: &, <, >, ", and '. Encoding & first is important — doing it last would double-encode the entities just produced by the other replacements, so the order in the chain matters. With this change, a file named report<img src=x onerror=alert(1)>.pdf is rendered as literal text (report&lt;img src=x onerror=alert(1)&gt;.pdf) instead of being parsed as a live <img> tag, regardless of whether it arrived via file.name or the file.path substring computed a few lines above.

Key Takeaways

  • escapeQuotes() and escapeBackSlash() are not HTML sanitizers — they protect quoted string literals, not markup context, and reusing them as if they were general-purpose output encoding is exactly what created this bug.
  • File names and paths returned by the file system (file.name, file.path) are attacker-controllable data on any platform that lets users rename files, and must be treated the same as any other untrusted input before hitting the DOM.
  • When a value crosses from "file metadata" to "rendered HTML," the encoding has to happen at that boundary — the fix inserts the escaping right where text is finalized, immediately before it enters the results array that feeds the UI.
  • Order matters when chaining .replace() calls for entity encoding: & must be escaped first, or the entities produced by escaping </>/"/' get double-encoded.

How Orbis AppSec Detected This

  • Source: file-system metadata (file.name and file.path) consumed inside the search(text) function
  • Sink: the search-results renderer that inserts the computed text value into the DOM as part of the results list
  • Missing control: HTML entity encoding of <, >, &, ", and ' — only quote and backslash escaping (escapeQuotes, escapeBackSlash) were applied
  • CWE: CWE-79, Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)
  • Fix: a chained .replace() call HTML-encodes the display text before it is pushed into the results array

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 case is a good reminder that "we already escape it" is not the same as "we escape it for the right context." The search() function had escaping logic in place — it just protected against the wrong set of characters for where the output actually landed. Because file names are effectively free-form attacker input on most systems, any UI that displays them needs explicit, context-appropriate output encoding, not string helpers borrowed from a different part of the codebase. The one-line fix here — HTML-encoding text right before it's stored in results — closes that gap without touching the rest of the search logic.

TAGS: xss, cwe-79, output-encoding, javascript, dom-manipulation, file-search

ANSWER_SUMMARY: The affected code is first-party JavaScript in a search(text) function that builds display text from file-system metadata (file.name, file.path). An attacker who can name a file (e.g., <img src=x onerror=alert(1)>.pdf) gets that markup rendered live in the app's search results, achieving script execution in the UI's DOM context. The fix adds a chained .replace() call that HTML-entity-encodes &, <, >, ", and ' before the text is pushed into the results array; no separate version is tracked since this is first-party code. This is classified under CWE-79, Improper Neutralization of Input During Web Page Generation.

VULNERABILITY_AT_A_GLANCE:
Vulnerability: Cross-Site Scripting (XSS) via unescaped file names in search results
CWE: CWE-79
Language: JavaScript
Risk: Malicious file/folder names execute as HTML/JS when displayed in search results
Root cause: escapeQuotes()/escapeBackSlash() only escape quotes and backslashes, not HTML markup characters
Fix: Added entity-encoding replace chain for &, <, >, ", ' before the text is stored in results

PRIMARY_ENTITY:
cve: N/A
ghsa: N/A
package: N/A
ecosystem: N/A
fixed_in: N/A
cwe: CWE-79

FAQ:
Q: Does the fix in the search() function replace escapeQuotes() and escapeBackSlash()?
A: No, both helper functions are left in place; the fix adds an additional .replace() chain that HTML-encodes &, <, >, ", and ' on top of the existing quote/backslash escaping.

Q: Where exactly is the new encoding applied relative to file.matchPath and file.name?
A: It's applied to the unified text variable right after it's computed from either file.path (when matchPath is true) or file.name, and before that value is pushed into the results array.

Q: Could a crafted folder name trigger this issue the same way as a file name?
A: Yes — the vulnerable code path used file.name for both files and folders when matchPath was false, so a malicious folder name would be rendered unescaped in exactly the same way.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #646

Related Articles

critical

DataTables RowGroup startRender XSS via Unescaped Group Data

DataTables RowGroup's default `startRender` callback inserted group labels directly into the DOM using HTML-aware methods, enabling XSS when user data reached the `dataSrc` property. The fix applies `util.escapeHtml()` to neutralize malicious payloads before insertion.

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.

high

brace-expansion DoS: Exponential Backtracking in Nested Brace Patterns

A critical vulnerability in brace-expansion allows attackers to cause denial of service by submitting specially crafted patterns with nested braces. The exponential-time complexity in pattern expansion creates a computationally expensive path that can freeze applications processing user-controlled input.