go-gomail / gomail

The best way to send emails in Go.
MIT License
4.4k stars 581 forks source link

Email content tampering vulnerability due to crafted file names #190

Open motoyasu-saburi opened 1 year ago

motoyasu-saburi commented 1 year ago

I know this package is not already maintained. However, the vulnerability does exist, and we will describe the issue here, including how to fix it.


The gopkg.in/gomail.v2 package has a problem with lack "filename" escaping in the Attachment File when sending an Email. This allows "file name/extension tampering" with attached "filename" field.

Essentially, the following characters must be escaped when generating Content-Disposition headers in multipart/mixed

These are also defined as requirements in RFCs and HTTP Requests for Multipart/form-data (What's WG - HTML Spec) where Content-Disposition is used.

RFC 6266: https://tools.ietf.org/html/rfc6266#appendix-D

Avoid including the "\" character in the quoted-string form of the filename parameter, as escaping is not implemented by some user agents, and "\" can be considered an illegal path character.

What's WG HTML Spec: https://html.spec.whatwg.org/#multipart-form-data

For field names and filenames for file fields, the result of the encoding in the previous bullet point must be escaped by replacing any 0x0A (LF) bytes with the byte sequence %0A, 0x0D (CR) with %0D and 0x22 (") with %22. The user agent must not perform any other escapes.

As an example, I describe the normal case and the case where a crafted file is processed.


normal case: Input Filename:

example.txt

Output Content-Disposition:

Content-Disposition: attachment; filename="example.txt"

Crafted file case: Input Filename:

a.sh";\r\n\r\ndummy.txt

Output Content-Disposition:

Content-Disposition: attachment; filename="a.sh";

ndummy.txt"

Attack Scenario

if " is not escaped, additional fields can be added, so that filenames can be tampered with, Content-Disposition size, modification-date, etc fields can be inserted, and so on.

I think there are two main cases of abuse.

  1. Cases where integrity is destroyed by rewriting emails sent by the victim by file name.
  2. Cases that pass validation such as file extension at the time of upload and are converted to malware (extension is changed) at the time of email transmission

(1) This case requires "strong" involvement of the victim, which I believe is a problem that is quite difficult to exploit.

The condition of the attack is that the filename of the attachment in the e-mail sent by the victim must be set to the string entered by the attacker.

However, since it is up to the system implementer to decide how to use this package and how to send the mail, we do not believe that it is absolutely certain.

(2) I believe this is the more likely case.

For example, let's say you have a system that uses this package behind a system such as ChatBot that receives customer communications and transfers files.

Since it is unlikely that customers will always send the correct file, file extension validation should be performed at the time of file upload, and those with no problems should be forwarded via email.

However, if this vulnerability exists, it is possible to rewrite the File Extension to .txt when uploading and .sh when sending mail.

Remediation

As far as I have been able to find, the following services are escaping by \.

example code:

def escapeContentDispositionFilename(filename: String): String = {
  return filename.replaceAll("\r", "\\r")
                 .replaceAll("\n", "\\n")
                 .replaceAll("\"", "\\\"")
}

example response:


> Before
> Content-Disposition: attachment; filename="malicious.sh";dummy=.txt

> After
> Content-Disposition: attachment; filename="\"malicious.sh\";dummy=.txt"

Also, eliminating CLRF or Double-Quote characters blocking them with Validation can be effective.

Reference

Golang Impliments (safe code) https://github.com/golang/go/blob/e0e0c8fe9881bbbfe689ad94ca5dddbb252e4233/src/mime/multipart/writer.go#L144

PoC

PoC Environment:

OS: macOS Monterey(12.3) LANG ver: go1.18.2 darwin/arm64 gomail ver: v2.0.0-20160411212932-81ebce5c23df

Receiver Email Client: Gmail


  1. Issue a Credential for SMTP via Gmail, etc.

e.g. a. Access: https://myaccount.google.com/apppasswords b. Select "Mail" & Other c. Click Generate & Copy "device application password" (Use the password in step 3)

  1. Install Package go get gopkg.in/gomail.v2

  2. Generate Vuln Code & PoC File

Generate PoC File

$ touch 'test.sh\"; dummy=\"a.txt'

Please Edit [Email, SMTP-Credential, FilePath]

package main

import (
    "gopkg.in/gomail.v2"
)

func main() {
    // FIXME
    EMAIL := "kumagoro_alice@yahoo.co.jp"
    CREDENTIAL := "fsfu dyny prtk mnax"
    FILE_PATH := "/Users/kumagoro/work/shopgo/test.sh\"; dummy=\"a.txt"

    m := gomail.NewMessage()
    m.SetHeader("From", EMAIL)
    m.SetHeader("To", EMAIL)
    m.SetHeader("Subject", "Hello!")
    m.SetBody("text/html", "<h1>Hello</h1>")
    m.Attach(FILE_PATH)

    d := gomail.NewPlainDialer("smtp.gmail.com", 587, EMAIL, CREDENTIAL)

    if err := d.DialAndSend(m); err != nil {
        panic(err)
    }
}
  1. Run Code.

  2. Check received mail source.

e.g. a. Access: https://mail.google.com/mail/u/0/ b. View received email. c. Click "..." button & Click "Show Original" button. d. Show "Content-Disposition" header.

multipart/mixed part example:

--680dadae3c58fb4a03d272dc4f68fb901d89541fca9e98e50a5bd97a5d52
Content-Disposition: attachment; filename="test.sh"; dummy="a.txt"
Content-Transfer-Encoding: base64
Content-Type: text/plain; charset=utf-8; name="test.sh"; dummy="a.txt"

--680dadae3c58fb4a03d272dc4f68fb901d89541fca9e98e50a5bd97a5d52--

Except Content-Disposition:

Content-Type: text/plain; charset=utf-8; name="test.sh\"; dummy=\"a.txt"

or

Content-Type: text/plain; charset=utf-8; name="test.sh%22; dummy=%22a.txt"

Actual Content-Disposition:

Content-Type: text/plain; charset=utf-8; name="test.sh"; dummy="a.txt"

Reference:

Relate RFC: https://datatracker.ietf.org/doc/html/rfc2231 https://datatracker.ietf.org/doc/html/rfc2616 https://datatracker.ietf.org/doc/html/rfc5987 https://datatracker.ietf.org/doc/html/rfc6266 https://datatracker.ietf.org/doc/html/rfc7578 https://datatracker.ietf.org/doc/html/rfc8187

WhatWG HTML Spec > multipart/form-data escape requirements: https://html.spec.whatwg.org/#multipart-form-data

Golang Impliments: https://github.com/golang/go/blob/561a5079057e3a660ab638e1ba957a96c4ff3fd1/src/mime/multipart/writer.go#L132-L136

Symphony (PHP Webframework) Impliments: https://github.com/symfony/symfony/blob/123b1651c4a7e219ba59074441badfac65525efe/src/Symfony/Component/Mime/Header/ParameterizedHeader.php#L128-L133

Spring (Java Webframework) Impliments: https://github.com/spring-projects/spring-framework/blob/4cc91e46b210b4e4e7ed182f93994511391b54ed/spring-web/src/main/java/org/springframework/http/ContentDisposition.java#L259-L267

Similar problem with another HTTP Client:

httparty: GHSA-5pq7-52mg-hr42 Playframework WS: https://github.com/playframework/playframework/pull/11571 Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1556711 OWASP ASVS v5 > Content-Disposition escape disscussion: https://github.com/OWASP/ASVS/issues/1390