Skip to content

net/smtp: Attachment.filename and cid are interpolated into MIME headers unsanitised, allowing CRLF header injection and DATA termination #30174

Description

@gabytz777

Summary

smtp's Attachment.to_string interpolates filename and cid straight into MIME headers with no sanitisation. A filename containing CRLF injects arbitrary headers into the outgoing message, or terminates the DATA transaction early.

Details

vlib/net/smtp/smtp.v:996-1004:

fn (a &Attachment) to_string() string {
	crlf := '\r\n'
	cid := if a.cid != '' {
		'Content-ID: <${a.cid}>${crlf}'
	} else {
		''
	}
	return 'Content-Type: application/octet-stream${crlf}${cid}Content-Transfer-Encoding: base64${crlf}Content-Disposition: attachment; filename="${a.filename}"${crlf}${crlf}${fold_base64(base64.encode(a.bytes))}'
}

This text is placed verbatim into the SMTP DATA payload.

The module is explicitly aware of this threat elsewhere:

  • smtp.v:355 strips CRLF with the comment "preventing header or protocol injection when the value is later placed into SMTP DATA"
  • Mail.subject is RFC2047-encoded when it contains control bytes (smtp.v:606)

Attachment.filename and Attachment.cid get neither treatment. Consequences:

  • A " in the filename terminates the quoted-string early and injects new parameters.
  • A \r\n in the filename injects arbitrary headers, for example a Bcc:.
  • A \r\n.\r\n terminates the DATA transaction early, after which the remainder of the attachment name is interpreted as further SMTP commands by the server.

The attacker-controlled value is the attachment filename, which in a web application is typically a user-supplied upload name.

Reproducer

import net.smtp

fn main() {
	mut c := smtp.new_client()
	c.connect('localhost', 25) or { panic(err) }
	c.send(smtp.Mail{
		from: 'a@b.c'
		to:   'x@y.z'
		body: 'hi'
		attachments: [smtp.Attachment{
			filename: 'x"\r\nBcc: victim@example.com\r\nX'
			bytes:    [1]
		}]
	})
}

The sent DATA contains:

Content-Disposition: attachment; filename="x"
Bcc: victim@example.com
X"

Suggested fix

Reject or strip CR and LF in both fields, and escape " and \ inside the quoted-string:

fn sanitize_filename(s string) string {
	s.replace('\r', '').replace('\n', '').replace('\\', '\\\\').replace('"', '\\"')
}

Rejecting is better than stripping: a filename containing a newline is not a legitimate filename. A guard at the top of Attachment.to_string (or in Mail.send) that returns an error for \r or \n in filename/cid matches what the rest of the module already does.

Environment

  • V 0.5.2, commit a60fb589
  • Verified byte-identical against current master (890d55b9)
  • Reported by reading vlib/net/smtp/smtp.v; the header assembly was traced by hand and is directly reproducible against any SMTP server

Activity

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions