I have a variable $filename that should be considered user input.
I use this $filename in the following:
header('Content-Disposition: inline; filename="' . $filename . '"');
How would one need to encode this to render $filename safe even when containing a payload?
Edit: From what I have been able to find so far, and what is provided by OWASP here, I may just need to filter all newline and form-feed characters? Looking for confirmation or additional info. Those being the only requirements, the following should be sufficient:
preg_replace('/[\f\r\n]/', '', $filename);
Edit: For this question assume validation has already been performed but a payload has made it through this without being rejected.
Related
I've been trying, for some time now, to export a properly encoded and formated CSV file with PHP. But it's just not working. I've tried every tip in every CSV/PHP related thread on SOF, I've checked that the data in my database is UTF-8, it is. I've tried stuff like utf8_encode() on the whole CSV-line, I've checked that the actual PHP file is encoded in UTF-8, but still no success. When I run the file on http://csvlint.io/ I just get:
Your CSV appears to be encoded in ASCII-8BIT. We recommend you use UTF-8.
But I can't find a trace of any other encoding than UTF-8 anywhere in my code..
Basically this is my code:
First, I put all my CSV-rows in an array, then do this:
if (count($array) == 0)
{
return NULL;
}
ob_start();
$df = fopen("php://output", 'w');
$csv = utf8_encode("header1|header2|header3|header4|header5|header6|header7\r\n");
foreach($array as $line) {
$csv .= $line . "\r\n";
}
setlocale(LC_ALL, 'sv_SE', "swedish");
fwrite($df, "\xEF\xBB\xBF".$csv);
fclose($df);
return ob_get_clean();
And these are the headers sent:
$now = gmdate("D, d M Y H:i:s");
header("Expires: Tue, 03 Jul 2001 06:00:00 GMT");
header("Cache-Control: max-age=0, no-cache, must-revalidate, proxy-revalidate");
header("Last-Modified: {$now} GMT");
header("Content-Encoding: UTF-8");
header("Content-Type: text/csv; charset=UTF-8");
header("Content-Type: application/force-download");
header("Content-Type: application/octet-stream");
header("Content-Type: application/download");
header("Content-Disposition: attachment;filename={$filename}");
header("Content-Transfer-Encoding: binary");
Any ideas?
The issue is the byte-order mark you're prepending to the output in this line:
fwrite($df, "\xEF\xBB\xBF".$csv);
If you change this to simply
fwrite($df, $csv);
You should find the resulting file validates just fine (or at least, the validator doesn't complain about its encoding).
Arguably this is a problem with the validator, since as the Wikipedia article notes,
The Unicode Standard permits the BOM in UTF-8, but does not require or recommend its use.
I don't recommend you use it either, as most software seems not to recognize byte-order marks. But if you must or you simply prefer to, you can safely ignore the warning from CSVLint.
Since that is apparently not the issue, the next thing I'd look at is whether or not the data is being retrieved from the database in UTF-8. (I'll take your word you've already checked carefully to make sure the data is being stored in UTF-8.) If you're using MySQL, this will depend on the configuration of the database server and any options you may be sending the database after connection.
The PHP manual has a section on character sets and MySQL, and there is also this helpful article about using PHP and MySQL together with UTF-8 data. If you're using a different database system, it likely has equivalent configuration options that should be checked.
The only other suggestions I can make are that you
Move the call to setlocale higher in the script, before string concatenation begins in the foreach loop. (I don't think this setting affects simple concatenation, but I'm not certain.)
Remove the Content-Encoding header from your output, as it is invalid the way it is currently being used.
Try to use this code:
$filename = 'csv/'.date('Y-m-d_H:i:s').'.csv';
$fp = fopen($filename, 'w');
foreach ($csvData as $fields) {
fprintf($fp, chr(0xEF).chr(0xBB).chr(0xBF));
fputcsv($fp, $fields, $delimiter = ';');
}
fclose($fp);
For science.
Say I have the following code:
<?php
$filename = $_GET['filename'] . '.csv';
$handle = #fopen($filename);
We know that the null byte exploit is long gone, but is it possible to get around the above appending of .csv, in order to read a file with another extension? Very creative souls exist.
Reading remote files works, filename=http://example.com/some.csv (.csv is appended automatically).
If you query for http://example.com/some.pdf?csv, fopen will try to gather the pdf file...
Rather use regex to validate $_GET (you should always validate your input):
/(\.csv)$/g will help you validate whether the extension is .csv
Using Symfony 2.5 here, users upload MS Office files into our application and download it later as needed. Now, when the file attachment contains non-ascii chars (which is quite common as we are from Czech republic) Symfony raises error 'The filename fallback must only contain ASCII characters.'
I found many reports of this problem and discussions e.g.
https://github.com/symfony/symfony/issues/9093
... but no real solution. I know I can convert filename to ascci when making Content-Disposition header but it changes the filename presented to user which is not nice and quite misleading for user. Is there way how to avoid this and be able to serve former file name? Being able to download file with non-ascii chars in filename is quite common on internet so why this restriction?
By following this How to encode the filename parameter of Content-Disposition header in HTTP? I even tried to encode filename with urlencode() but now it says the % is not allowed char :-(
Update 1: Here is a code snippet I'm using. I'm using streaming the response to browser and headers are defined rather manualy I think.
$response = new StreamedResponse();
$response->headers->set('Content-Type', $upload->getMimeType());
$contentDisposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $upload->getFilename());
$response->headers->set('Content-Disposition', $contentDisposition);
Symfony 4 has $filenameFallback in HeaderUtils::makeDisposition.
Example
$filenameFallback = preg_replace(
'#^.*\.#',
md5($filename) . '.', $filename
);
$disposition = $response->headers->makeDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
$filename,
$filenameFallback
);
$response->headers->set('Content-Disposition', $disposition);
I have the following URL:
http://www.solutionssoftwarematrix.com/download.php?filename=CreatingandMarketingthePerfectYouTubeVideo.zip
I am trying to pass the string variable in to the PHP script (below) when someone clicks the link above.
Here is the script (download.php):
http://www.devshed.com/c/a/PHP/Simple-and-Secure-PHP-Download-Script-with-Limits-Tutorial/1/
THE LINES BELOW with BOLD AREAS are where my $_GET FUNCTION is BEING ADDED, however, I keep receiving parse errors OR it does not pull the filename, and the download does not work.
header("Content-type:application/pdf");
header('Content-Disposition: attachment;filename="http://www.yourdomain.com/ebookfordownloads/$_GET['$filename']"');
readfile("/your/absolute/server/path/html/ebookfordownloads/$_GET['$filename']");
header("Content-type:application/pdf");
header('Content-Disposition: attachment; filename="http://www.yourdomain.com/ebookfordownloads/$_GET['$filename']"');
readfile("/your/absolute/server/path/html/ebookfordownloads/$_GET['$filename']");
There are multiple problems:
This syntax is invalid: '$_GET['$filename ... The apostrophe before '$filename closes the string. What you probably want to do is downloads/' . $_GET[$filename] -- concatenation
This syntax is also invalid: "downloads/$_GET['$filename" -- with array access in a quoted string, you cannot have an apostrophe. Again, concatenation is your best bet: "downloads/" . $_GET[$filename]
I'm not sure whether you want $_GET['$filename'] or $_GET[$filename] or $_GET['filename']. There is a very important difference between the three.
Your code has a large vulnerability if a user can set $filename somehow (or the filename get parameter, if that is what you intend to use).
You are not getting basic PHP string functionality. To add variables like that, you'll have to use this:
readfile("/your/absolute/server/path/html/ebookfordownloads/{$_GET['$filename']}");
or concat them:
readfile("/your/absolute/server/path/html/ebookfordownloads/".$_GET['$filename']);
Not to mention the Very Big Security Hole when using user input ($_GET variables) verbatim in your code. Do validation before you use the given information.
Suppose this is how it should be:
header("Content-type:application/pdf");
header('Content-Disposition: attachment;filename="http://www.yourdomain.com/ebookfordownloads/' . $_GET['filename'] . '"');
readfile("/your/absolute/server/path/html/ebookfordownloads/" . $_GET['filename']);
header("Content-type:application/pdf");
header('Content-Disposition: attachment; filename="http://www.yourdomain.com/ebookfordownloads/' . $_GET['filename'] . '"');
readfile("/your/absolute/server/path/html/ebookfordownloads/" . $_GET['filename'] );
Take a look at string concatenation in PHP.
Also: $_GET['$filename'] means that you are asking PHP to get everything after ?$filename= in url. But your url has no $filename parameter, only filename, so it should be modified to $_GET['filename']. Or, if you have a name of parameter stored in variable called $filename like this: $filename = 'filename', it should be changed to $_GET[$filename]
I am using the following script http://stuporglue.org/recieve-e-mail-and-save-attachments-with-a-php-script/ to handle my emails that get sent to me, however it seems if a user sends an email from mail or outlook or any email client that sends base64 attachments they are not being saved in the data base, and the body text of the email is also skiped.
I am wondering if anyone sees an error in the code, as I have looked and dont see anything that sticks out.
a closer look shows the following
Mail.app sends its base64 like this.
--Apple-Mail=_9E76B10A-4086-43B8-B835-78F184FA43FC
Content-Disposition: inline;
filename=CV-IT.pdf
Content-Type: application/pdf;
name="CV-IT.pdf"
Content-Transfer-Encoding: base64
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
1NVM1hk3kE1G3rlPZKL2CeZ+Hdp
while Gmail sends it like this.
Content-Type: application/pdf; name="CV-IT.pdf"
Content-Disposition: attachment; filename="CV-IT.pdf"
Content-Transfer-Encoding: base64
X-Attachment-Id: f_gx86pbon0
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
1NVM1hk3kE1G3rlPZKL2CeZ+Hdp
The gmail attachment saves while the mail.app does not.
disclaimer the problematic script in question has more issues, which I will not address, and the answer below is meant as a quick fix for the problem at hand, while hoping to enlighten some readers who were not able to diagnose the problems themselves. /disclaimer
There are two main problems.
Problem 1: split header lines
Look at these headers:
Content-Disposition: inline;
filename=CV-IT.pdf
Content-Type: application/pdf;
name="CV-IT.pdf"
versus
Content-Type: application/pdf; name="CV-IT.pdf"
Content-Disposition: attachment; filename="CV-IT.pdf"
Now look at the part that processes these lines:
$info = split("\n",$parts[0]);
..
foreach($info as $line)
{
if( preg_match("/Content-Type: (.*);/",$line,$matches) )
{
$type = $matches[1];
}
if( preg_match("/Content-Disposition: attachment; filename=\"(.*)\"/",
$line,$matches) ) {
$name = time() . "_" . $matches[1];
}
..
}
This splits the header in lines, and then tries to match every line. Now look at the two headers. The second (working) one has 2 lines, which are perfectly matched.
The first (not working) one has 4 (!) lines. None of these 4 lines matches the patterns.
There are countless ways of tackling this problem, and I'll take a quick&dirty oneliner. Add this line before $info = split("\n",$parts[0]);
$parts[0] = preg_replace("/\r?\n\s+/"," ",$parts[0]);
it will turn the split headers into oneliners again by looking for newlines followed by whitespace, and replacing them with just one space.
Problem 2: wrong pattern
Assuming you applied the fix above, you have this pattern:
if( preg_match("/Content-Disposition: attachment; filename=\"(.*)\"/", ...
trying to match this line:
Content-Disposition: inline; filename=CV-IT.pdf
Two things go wrong here:
problem 2a: disposition inline/attachment
The pattern clearly looks for the word "attachment", while the line says "inline". This is fixed by replacing attachment by (attachment|inline), which indicates an alternative. (note that this also captures the disposition type)
problem 2b: filename double quotes
The pattern further looks for filename="(.*)", while the line has a filename without the quotes.
This is also no major issue, if you insert ? after the ", to indicate that the " is optional, all will work. To make it perfect, you must also ensure that the . will not match the ending " if available, so replace filename="(.*)" with:
filename="?([^"]+)"?
where [^"]+ stands for 'anything but "'.
So if you change these lines:
if( preg_match("/Content-Disposition: attachment; filename=\"(.*)\"/",
$line,$matches) ) {
$name = time() . "_" . $matches[1];
}
into
if( preg_match('/Content-Disposition: (attachment|inline); filename="?([^"]*)"?/',
$line,$matches) ) {
$disposition = $matches[1];
$name = time() . "_" . $matches[2];
}
it should work. (note that I changed the pattern to use single quotes, so that you need not escape the double quotes, making things legible)
To make this script fool proof, you should really read the appropriate RFCs, to see what more is to be expected in email headers. This script has a lot of assumptions buried in it.
The problem is that my script is not looking for inline content, only for attached content. With the way you have attached the file, it is inline, hence the
Content-Disposition: inline;
filename=CV-IT.pdf
If you attach it, you would instead see
Content-Disposition: attachment; filename="CV-IT.pdf"
The Content-Disposition handling is around line 54-64 of the script on my site (linked in original question).
seems you used preg_match to get the boundary of the mail near line 166:
if (preg_match("/boundary=(.*boundary)$/",$line,$matches)){
$boundary = $matches[1];
You used the "/" character as delimiter of pattern of regular expression and you have the "/" in your boundary content at the same time.
So this may be the reason why your code does not work.
Try this:
if (preg_match("{boundary=(.*boundary)$}",$line,$matches)){
$boundary = $matches[1];