Skip to content

Commit 3975b08

Browse files
committed
N°4281 - "Content Type" and "Content Disposition" RFC 2045-compliant for "name" and "filename" attributes.
1 parent 73ead6f commit 3975b08

File tree

2 files changed

+121
-9
lines changed

2 files changed

+121
-9
lines changed

classes/rawemailmessage.class.inc.php

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,21 @@ public function GetMessageId()
172172
*/
173173
public function GetAttachments(&$aAttachments = null, $aPart = null, &$index = 1)
174174
{
175+
// This Regex complies with RFC 2045 regarding the Grammar of Content Type Headers Filenames:
176+
// (1) Allow all chars for the filename, if the filename is quoted with double quotes
177+
// (2) Allow all chars for the filename, if the filename is quoted with single quotes
178+
// (3) If the filename is not quoted, allow only ASCII chars and exclude the following
179+
// chars from the set, referenced as "tspecials" in the RFC:
180+
// <ALL-CTL-CHARS-INCL-DEL>
181+
// <CHAR-SPACE>
182+
// ()<>@,;:\"/[]?=
183+
// To keep the regex as simple as possible, the _allowed_ chars are whitelisted
184+
// with their corresponding hexval.
185+
$sFileNameRegex = <<<REGEX
186+
(("([^"]+)")|('([^']+)')|([\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E]+))
187+
REGEX
188+
;
189+
175190
static $iAttachmentCount = 0;
176191
if ($aAttachments === null)
177192
{
@@ -188,17 +203,10 @@ public function GetAttachments(&$aAttachments = null, $aPart = null, &$index = 1
188203
{
189204
$sFileName = '';
190205
$sContentDisposition = $this->GetHeader('content-disposition', $aPart['headers']);
191-
if (($sContentDisposition != '') && (preg_match('/filename="([^"]+)"/', $sContentDisposition, $aMatches)))
206+
if (($sContentDisposition != '') && (preg_match('/filename='.$sFileNameRegex.'/', $sContentDisposition, $aMatches)))
192207
{
193208
$sFileName = $aMatches[1];
194209
}
195-
else
196-
{
197-
if (($sContentDisposition != '') && (preg_match('/filename=([^"]+)/', $sContentDisposition, $aMatches))) // same but without quotes
198-
{
199-
$sFileName = $aMatches[1];
200-
}
201-
}
202210

203211
$bInline = true;
204212
if (stripos($sContentDisposition, 'attachment;') !== false)
@@ -223,10 +231,17 @@ public function GetAttachments(&$aAttachments = null, $aPart = null, &$index = 1
223231
{
224232
$sType = $aMatches[1];
225233
}
226-
if (empty($sFileName) && preg_match('/name="([^"]+)"/', $sContentType, $aMatches))
234+
if (empty($sFileName) && preg_match('/name='.$sFileNameRegex.'/', $sContentType, $aMatches))
227235
{
228236
$sFileName = $aMatches[1];
229237
}
238+
// Note: Since the RFC2045-compliant regex above has different levels of matches,
239+
// because of the use of capture groups, indexing into the results with
240+
// $aMatches[1] sometimes returns the filename with quotes, but sometimes not.
241+
// For that reason (and for the sake of codesimplicity) we strip all quotes
242+
// from filenames here and will be fine.
243+
$sFileName = str_replace("'", '', $sFileName);
244+
$sFileName = str_replace('"', '', $sFileName);
230245
if (empty($sFileName))
231246
{
232247
// generate a name based on the type of the file...
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
Return-Path: <[email protected]>
2+
Received: from www192.your-server.de
3+
by www192.your-server.de with LMTP
4+
id cANPJDAmYmNaVQEAUXDXKw
5+
(envelope-from <[email protected]>); Wed, 02 Nov 2022 09:11:28 +0100
6+
Envelope-to: [email protected]
7+
Delivery-date: Wed, 02 Nov 2022 09:11:28 +0100
8+
Authentication-Results: www192.your-server.de;
9+
iprev=pass (localhost) smtp.remote-ip=127.0.0.1;
10+
spf=pass smtp.mailfrom=itomig.de;
11+
dkim=pass header.d=itomig.de header.s=default2012 header.a=rsa-sha256;
12+
dmarc=skipped
13+
Received: from localhost ([127.0.0.1] helo=www192.your-server.de)
14+
by www192.your-server.de with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384
15+
(Exim 4.94.2)
16+
(envelope-from <[email protected]>)
17+
id 1oq8qO-000NG0-7r
18+
for [email protected]; Wed, 02 Nov 2022 09:11:28 +0100
19+
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=itomig.de;
20+
s=default2012; h=Subject:From:To:MIME-Version:Date:Message-ID:Content-Type:
21+
Sender:Reply-To:Cc:Content-Transfer-Encoding:Content-ID:Content-Description:
22+
Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:
23+
In-Reply-To:References; bh=qfsa/c5xPqZ3d+6wNfzUExTW0ZNkPkALI3o0hp2R85A=; b=r5
24+
zbg+7XSo/HBfaFAqqdBD0gDiOIa8ujhqmikWoXps5jf9oMJVJ6af5ynLdfZPbxkpWss5t6os7LhO/
25+
w/ZdvjR8sOpvIX5nkR3DJxnG76Z28p0aFtpBz1OSIXpWe7LmSj8mBwsXBuh6jTLLH2xRGY3gBEIU9
26+
fkOlRbJQ6lU3qhS9Xa9cgSGECPfhIvZzdM/Rb0vdvvERCmrpvr9TylqFb+3RA05pQJBOvG3i/ih7O
27+
5a9RytEsd5rpN1IabPoMV6UJFhKaxPY4hMG4qdgzI2GQZAcoK5kNm/IrUxeHQ8KLy5HigJ1I11e9e
28+
kzqzy6hT0ZKrELFcqh6Y8L6EzgtC+aOg==;
29+
Received: from [95.90.147.54] (helo=[192.168.0.103])
30+
by www192.your-server.de with esmtpsa (TLS1.3) tls TLS_AES_256_GCM_SHA384
31+
(Exim 4.94.2)
32+
(envelope-from <[email protected]>)
33+
id 1oq8qO-000NFs-3t
34+
for [email protected]; Wed, 02 Nov 2022 09:11:28 +0100
35+
Content-Type: multipart/mixed; boundary="------------8PrwbWLJ6Ep94lElB18RfVAR"
36+
Message-ID: <[email protected]>
37+
Date: Wed, 2 Nov 2022 09:11:25 +0100
38+
MIME-Version: 1.0
39+
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
40+
Thunderbird/102.2.2
41+
Content-Language: en-US
42+
43+
From: Martin Raenker <[email protected]>
44+
Subject: test for attachments with unquoted name
45+
X-Authenticated-Sender: [email protected]
46+
X-Virus-Scanned: Clear (ClamAV 0.103.7/26707/Tue Nov 1 21:23:26 2022)
47+
X-local-sign: yes
48+
X-DKIM-Status: pass [(itomig.de) - 127.0.0.1]
49+
Delivered-To: [email protected]
50+
51+
This is a multi-part message in MIME format.
52+
--------------8PrwbWLJ6Ep94lElB18RfVAR
53+
Content-Type: text/plain; charset=UTF-8; format=flowed
54+
Content-Transfer-Encoding: 7bit
55+
56+
testbody
57+
--------------8PrwbWLJ6Ep94lElB18RfVAR
58+
Content-Type: application/octet-stream; charset=UTF-8; name="example_attachment_mail.csv"
59+
Content-Disposition: attachment;
60+
Content-Transfer-Encoding: base64
61+
62+
InRlc3QiLCJ0b3N0Igo=
63+
64+
--------------8PrwbWLJ6Ep94lElB18RfVAR
65+
Content-Type: application/octet-stream; charset=UTF-8; name='example_attachment_mail.csv'
66+
Content-Disposition: attachment;
67+
Content-Transfer-Encoding: base64
68+
69+
InRlc3QiLCJ0b3N0Igo=
70+
71+
--------------8PrwbWLJ6Ep94lElB18RfVAR
72+
Content-Type: application/octet-stream; charset=UTF-8; name=example_attachment_mail.csv
73+
Content-Disposition: attachment;
74+
Content-Transfer-Encoding: base64
75+
76+
InRlc3QiLCJ0b3N0Igo=
77+
78+
--------------8PrwbWLJ6Ep94lElB18RfVAR
79+
Content-Type: application/octet-stream; charset=UTF-8;
80+
Content-Disposition: attachment; filename="example_attachment_mail.csv"
81+
Content-Transfer-Encoding: base64
82+
83+
InRlc3QiLCJ0b3N0Igo=
84+
85+
--------------8PrwbWLJ6Ep94lElB18RfVAR
86+
Content-Type: application/octet-stream; charset=UTF-8;
87+
Content-Disposition: attachment; filename='example_attachment_mail.csv'
88+
Content-Transfer-Encoding: base64
89+
90+
InRlc3QiLCJ0b3N0Igo=
91+
92+
--------------8PrwbWLJ6Ep94lElB18RfVAR
93+
Content-Type: application/octet-stream; charset=UTF-8;
94+
Content-Disposition: attachment; filename=example_attachment_mail.csv
95+
Content-Transfer-Encoding: base64
96+
97+
InRlc3QiLCJ0b3N0Igo=

0 commit comments

Comments
 (0)