PHP:在使用&(和号)时,readfile()会导致下载损坏

3
我希望制作一个可以下载的PDF文件,但无法通过直接访问URL进行下载。
链接应该长成这样。
<a href="getFile.php?file=myfile.pdf" >Myfile</a>

而这就是getFile.php

<?php

  $file=$_GET['file'];

  // Check if file really exists:
  $files = array_diff(scandir('/www/secretPlace/'), array('.', '..'));
  $show = (in_array($file,$files)) ? true : false;
  if(!$show) exit();

  header('Content-Type: application/pdf');
  header('Content-Disposition: attachment; filename="' . $file . '"');
  readfile( '/www/secretPlace/' . $file);

只要PDF文件名不包含 & 符号,这个操作就可以正常工作。如果尝试使用

<a href="getFile.php?file=myfile & other stuff.pdf" >My Special File</a>

我可以下载带有文件名的文件,但无法使用pdf阅读器打开。所有程序都告诉我文件已损坏。如何使用readfile打开带有&符号的pdf文件?


5
不要在函数中传递用户输入;如果这样做,用户可以从您的服务器下载任何文件。 - itzmukeshy7
@itzmukeshy7 我已经添加了一个检查,用于确定文件是否实际存在于目录中。你认为现在安全了吗?否则我考虑使用 https://dev59.com/wW855IYBdhLWcg3wrGZY#4205278。 - Adam
不需要添加foreach循环,只需使用in_array()进行检查即可。 - itzmukeshy7
@itzmukeshy7,感谢您的好建议。我已经改正了。 - Adam
2个回答

2
你的代码容易受到路径遍历攻击。我强烈建议你明确告诉用户可以下载哪些文件。
对于你的问题(更新后),我建议你将文件名(不需要编码)存储在数据库中,每一行都有一个单独的ID。你可以创建一个页面,比如说 /download/1234 对应 ID 1234,这样你就可以自动知道它属于哪个文件,而不必担心特殊字符会破坏URI字符串。URL结构可以在.htaccess中进行配置,/download/[1-9][0-9]* 指向你在问题中提到的PHP页面,在那里你应该处理所有这些事情。然后你需要从数据库中获取文件名,使用 glob() 检查是否存在,并使用 Content-Disposition 开始用户下载。
如果你不喜欢数据库解决方案,你需要对URI字符串中的特殊字符进行编码(尤其是像你提到的 & 这样的字符),或者编写一个手动解析URI字符串的函数,而不是依赖于 $_GET,因为它会在渲染时在 &= 处分割文本。
关于安全方面的扩展问题:
即使不应该匹配,我也可以提供文件名。
if($pdf == $file) $show = true;               

如果您使用三个等号,那么就更好了,但仍然可能容易受到C空字节注入攻击的影响。因此,建议始终使用白名单而不是黑名单(在本例中是..)来进行输入验证。


你建议我为每个单独的PDF制作一个页面吗?因为我正在寻找一种动态解决方案。我还在我的getFile.php中添加了一个检查,以确保文件实际存在于文件夹中(就在我看到你的答案之前的1秒钟),所以我认为它不再容易受到路径遍历攻击,对吧? - Adam
这是一个足够动态的解决方案,如果你用正确的方式使用.htaccess。你需要创建一个单独的PHP页面。 - Rápli András
它仍然容易受到特定注入攻击(CR、LF、null-byte)。(作为建议,您永远不要将不良输入列入黑名单,而是始终将好的输入列入白名单)。如果您想使特定的下载目录可用,请使用“glob”读取文件名并将其列入白名单。 - Rápli András
好的。感谢您的努力。但我还有两个问题:1)我不完全理解您对我的主要问题的回答 - 您基本上是说,我应该避免使用函数readfile,并将pdf的内容保存在某个我必须动态创建的php页面中?然后,通过.htacces,我必须将pdf文件名映射到包含变量中的pdf的动态创建的php页面? - Adam
你说:“我强烈建议你明确告诉用户可以下载哪些文件。”当我遍历文件夹中的所有可能文件名并将它们与用户输入的$file比较时,你说这还不够。那我该怎么办呢?我的意思是,即使我有一个手写的允许使用的文件名数组,最终我也需要将其与用户输入进行比较,不是吗?我不是在质疑你的能力,我只是想了解我应该做什么。对于造成的困惑,请原谅。 - Adam
显示剩余4条评论

1
它没有起作用,因为在使用时出了问题。
<a href="getFile.php?file=myfile & other stuff.pdf" >My Special File</a>

那么

$file=$_GET['file'];

这个URL只有myfile这个值。需要像这样解码URL:

<a href="getFile.php?file=<?php echo urlencode('myfile & other stuff.pdf'); ?>" >My Special File</a>

安全白名单方法可以被https://dev59.com/wW855IYBdhLWcg3wrGZY#4205278中解释的更安全的方法所替代。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接