Skip to content

fix(drivers): a quote in an attachment filename injects a second filename parameter into Content-Disposition #122

Closed

Description

@productdevbook

What happens

renderAttachment() interpolates the filename into two quoted MIME parameters without escaping the quote character:

const name = encodeHeaderValue(attachment.filename)
`Content-Type: ${contentType}; name="${name}"`,
`Content-Disposition: ${disposition}; filename="${name}"`,

encodeHeaderValue() only base64-encodes when the value contains a character outside \x20-\x7E. A plain-ASCII " passes through untouched, so a filename can close the quoted string and append parameters of its own.

Reproduction

import { buildMime } from "../src/drivers/_mime.ts"

const out = buildMime({
  from: { email: "hi@acme.com" }, to: [{ email: "a@x.com" }], cc: [], bcc: [], replyTo: [],
  subject: "hi", text: "x", messageId: "<1@a.com>", date: new Date(0),
  attachments: [{
    filename: 'report.txt"; filename="payload.exe',
    content: "AAAA",
    contentType: "text/plain",
  }],
})
console.log(out.body.split("\r\n").filter((l) => l.startsWith("Content-Disposition") || l.startsWith("Content-Type: text/plain; name")))

Observed output:

[
  'Content-Type: text/plain; name="report.txt"; filename="payload.exe"',
  'Content-Disposition: attachment; filename="report.txt"; filename="payload.exe"'
]

Two filename parameters on one header. Which one a mail client honours is client-specific; several take the last.

Why it matters

Applies to the SMTP and SES paths. Where the filename comes from a user upload — the common case for "email me this file" flows — the uploader chooses the name the recipient's client displays and saves under, including its extension. A file the sending application believes is report.txt can be presented to the recipient as payload.exe.

Note the injection is limited to parameter confusion, not full header injection: a filename containing CR or LF falls outside \x20-\x7E and gets base64-encoded, which happens to close that hole. The ASCII " is the one that gets through.

Separately and less severely, the same two lines put an RFC 2047 encoded-word inside a quoted parameter value (filename="=?utf-8?B?…?="), which RFC 2047 §5 forbids — RFC 2231 filename*=UTF-8''… is the correct encoding for a non-ASCII filename.

Where

  • src/drivers/_mime.ts:182-195 — renderAttachment()
  • src/drivers/_mime.ts:226-229 — encodeHeaderValue() lets ASCII " through

Suggested fix

Never interpolate a caller-supplied string into a quoted MIME parameter unescaped. Escape " and \ (RFC 5322 quoted-string rules) before interpolating, and switch the non-ASCII case to RFC 2231 filename*=UTF-8''<percent-encoded> — which sidesteps quoting entirely and fixes the encoded-word-in-a-parameter problem at the same time.

Activity

  1. productdevbook commented on Sep 2, 2026

    @productdevbook
    OwnerAuthor

    Fixed in #130, with a regression test. Suite went from 269 tests to 293.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions