【问题标题】:Storing PHP(/PHP-FPM/Apache)'s temporary-from-upload files in RAM rather than the filesystem (or encrypted only)?将 PHP(/PHP-FPM/Apache) 的临时上传文件存储在 RAM 中而不是文件系统中(或仅加密)?
【发布时间】:2011-08-07 18:34:59
【问题描述】:

原问题

所以我正在从事的项目对文件上传非常偏执。
在这个问题的范围内,关于有效载荷,我没有使用该术语。我说的是机密性

程序总是会崩溃并在文件系统中留下临时文件。这很正常。 稍微保密性偏执狂可以编写一个 cronjob,每隔几分钟就会访问临时文件夹,并在 cronjob 调用之前删除任何早于几秒钟的内容(不是所有内容,只是因为否则它可能会捕获正在上传的文件)。

...不幸的是,我们更进一步:

理想情况下,我们希望永远不会在与进程相关的 RAM 中的任何地方看到来自文件上传的临时文件。

有没有办法教 PHP 在内存中而不是在文件系统中寻找临时文件? 我们使用 PHP-FPM 作为 CGI 处理程序,使用 Apache 作为我们的网络服务器,以防万一让它变得更容易。 (另请注意:'Filesystem' 是这里的关键字,而不是 'disc',因为当然有将文件系统映射到 RAM 的方法,但这并不能解决可访问性和自动崩溃后清理问题。 )

或者,有没有一种方法可以在将这些临时文件写入光盘时立即加密,这样它们就不会被保存在文件系统中没有加密?


线程概览

很遗憾,我只能接受 一个 的答案 - 但对于阅读本文的任何人来说,整个帖子非常 很有价值,并且包含许多人的集体见解。根据希望达到的目标,接受的答案您可能不感兴趣。如果您是通过搜索引擎来到这里的,请花点时间阅读整个帖子

以下是我看到的用例汇编,以供快速参考:

Re: PHP 的临时文件

  • RAM 而不是磁盘(例如,由于 I/O 问题)→RAMdisk/comparable(plasmid87Joe Hopfgartner

  • 立即(每个文件系统用户)加密 → encFS (ADW) (+ a gotcha 根据 Sander Marechal)

  • 安全文件权限 → restrictive native Linux permissions (optionally per vhost) (Gilles) 或 SELinux(参见各种 cmets)

  • 进程附加的内存而不是文件系统(因此进程崩溃会删除文件)(最初是由问题提出的)

回复:您的文件,上传后

【问题讨论】:

  • 有没有办法教 PHP 在内存而不是文件系统中寻找临时文件作为 blob? --> 使用 tmpfs 作为临时上传目录。
  • @Artefact2:关键字非常filesystem(请参阅您引用的段落的其余部分)。恐怕这对我一点帮助都没有。
  • 为什么不保护文件系统?在功能性用户 ID 下运行您的 php/apache 或其他任何内容,并允许该用户 ID 并且只有该用户 ID 访问临时目录。
  • @James Anderson:我想要每个进程甚至每个请求的东西,而不是每个程序。有强大的按程序解决方案(SELinux/EncFS/comparable),虽然不完美,但肯定会缓解问题,因此这绝对是一个将要追求的步骤,但仍然不是最佳的。
  • 无论是谁经历了所有的答案并否决了它们(这就是现在的样子,如果我判断错误,我深表歉意),我对你否决的一些答案投了赞成票,如果你说你为什么不给他们投票,所以我知道为什么不去追求那条路。

标签: php security apache file-upload temporary-files


【解决方案1】:

最明显的方法是:

我认为这没有任何问题。只要确保你分配了足够的空间。

您可以使用 LUKS 或 truecrypt 实时加密

编辑:

在您发表评论后,我想我现在明白您的问题了

apache/php 不支持这个。

但是,您可以编写自己的守护程序,打开套接字连接以侦听并以您想要的任何方式处理传入数据。基本上用 php 编写自己的网络服务器。 不应该做太多的工作。还有一些不错的课程。 zend 有一些便于 http 处理的服务器库。

但是你可以在 perl 中更容易地做到这一点。您可以逐块归档发布数据以处理关联的内存。 php 只是有不同的工作流程。

【讨论】:

  • 不,RAMdisc 和相关的“解决方案”仍然是文件系统解决方案。我不希望文件系统中的文件 - 我希望它在进程相关的内存中,进程崩溃将自动删除它。在文件系统中(无论是映射到磁盘还是 RAM),进程崩溃不会自动删除文件。
【解决方案2】:

我不熟悉 PHP,所以我的回答不会直接映射到操作方法,但我认为您对各种系统功能提供的保护存在一些误解,这导致您拒绝有效支持具有完全相同安全属性的解决方案的解决方案。从您的 cmets 中,我了解到您正在运行 Linux;我的大部分答案都适用于其他 unice,但不适用于其他系统,例如 Windows。

据我所知,您关注三种攻击场景:

  1. 攻击者获得了对机器的物理访问权,将其关闭,取出磁盘并读取其内容以供她随意使用。 (如果攻击者可以读取您的 RAM,那么您已经输了。)
  2. 攻击者可以在机器上以用户身份运行代码。
  3. CGI 脚本中的一个错误允许进程读取由其他进程创建的临时文件。

第一种攻击者可以读取磁盘上未加密的所有内容,而使用她没有的密钥加密的任何内容

第二种攻击者能做什么取决于她能否以运行 CGI 脚本的同一用户身份运行代码。

如果她只能以其他用户的身份运行代码,那么保护文件的工具就是权限。您应该有一个模式为 700 (= drwx------) 的目录,即只能由用户访问,并由运行 CGI 脚本的用户拥有。其他用户将无法访问此目录下的文件。您不需要任何额外的加密或其他保护。

如果她可以以 CGI 用户身份运行代码(当然,这包括以 root 身份运行代码),那么你已经输了。如果您以同一用户的身份运行代码,您可以看到另一个进程的内存——调试器一直都在这样做!在Linux下,你可以通过exploring /proc/$pid/mem自己轻松查看。与读取文件相比,读取进程的内存在技术上更具挑战性,但在安全方面,没有区别。

因此将数据保存在文件中本身并不是安全问题

现在让我们检查第三个问题。令人担忧的是,CGI 中的错误允许攻击者窥探文件但不能运行任意代码。这与可靠性问题有关 - 如果 CGI 进程终止,它可能会留下临时文件。但它更笼统:文件可能会被并发运行的脚本读取。

防止这种情况的最佳方法确实是完全避免将数据存储在文件中。这应该在 PHP 或其库级别完成,我对此无能为力。如果不可能,那么nullfs as suggested by Phil Lello 是一个合理的解决方法:PHP 进程会认为它正在将数据写入文件,但文件实际上不会包含任何数据。

这里还有另一个常见的 unix 技巧可能有用:创建文件后,您可以取消链接(删除)它并继续使用它。一旦它被取消链接,文件就不能以其以前的名称访问,但只要文件在至少一个进程中打开,数据就会保留在文件系统中。但是,这对可靠性非常有用,可以让操作系统在进程因任何原因终止时删除数据。可以使用进程权限打开任意文件的攻击者可以通过/proc/$pid/fd/$fd 访问数据。可以随时打开文件的攻击者在文件创建和取消链接之间有一个小窗口:如果她可以打开文件,那么她可以查看随后添加到其中的数据。尽管如此,这可能是有用的保护,因为它将攻击变成了一种对时间敏感的攻击,并且可能需要许多并发连接,因此可以通过连接速率限制器来应对或至少使其变得更加困难。

【讨论】:

  • @Gilles +1 关于 /proc//mem 的一个很好的观点。我基本上同意这里;然而,当涉及多个不同职责的团队时,有时需要提供增加安全性的错觉(即使让营销材料在技术上属实是一种捏造)——我不知道这是否属实这里。进程在从磁盘清除之前打开临时文件的可能性也很小,将其保留在磁盘上但不在目录中;我无法立即想到将数据锁定在内存中的类似方法。
  • @Phil:哦,进程可以打开文件并立即取消链接。但这不是为了安全(调试功能仍然允许访问该文件,并且在 Linux 下不需要ptrace,您可以在/proc/$pid/fd 中阅读它),这样文件将在进程中自动收割死得一塌糊涂。
  • 虽然我很欣赏您的担忧(它们通常非常有效,并且在安全方面非常有价值),但恐怕您确实错过了我想要实现的目标。这个想法是让文件在进程终止时被丢弃,而不必依赖于一个然而时钟的进程外 cron 作业或删除文件的其他功能。在这方面,将数据保存在文件中一个安全问题(尽管我同意这肯定不是唯一的问题)——它们可能会比以往任何时候都存在更长的时间。
  • 这绝对是不管它们的存储位置(磁盘或 RAM)。我根本不介意光盘部分。我介意进程外的部分。 :)
  • @pinkgothic:啊,我明白了。那么你想要的确实是首先不获取文件系统中的数据。让操作系统自动删除文件很容易,但还不够。加密不会给你带来任何好处,毕竟所有 PHP 进程都必须拥有密钥。
【解决方案3】:

我对此突然有了灵感:黑洞文件系统。

本质上,这是一个假文件系统,数据永远不会被写入,但所有文件都存在,并且没有内容。

有一个关于这些的 discussion on unix.se,而 one answer 涉及的只是这个的 FUSE 实现(在此处引用):

不支持开箱即用 我知道的任何 unix,但你可以做得很好 很多东西 FUSE。 至少有one implementation of nullfs¹, 每个文件都存在的文件系统 并且表现得像/dev/null(这个 不是我唯一的实现 见过)。

¹ 不要与 *BSD nullfs, 这类似于 bindfs

我没有机会对此进行测试 但是如果您将 upload_tmp_dir 设置为黑洞位置,则上传(将|应该)永远不会被写入到磁盘,但仍可在 $HTTP_RAW_POST_DATA(或 php://input)中使用。如果可行,那比打补丁 PHP 好

【讨论】:

  • 虽然这是可能的,但如果我正确理解问题,it's not actually necessary
  • 如果有人对此进行测试,请发表评论以分享结果 - 目前这是预期但未经测试的行为
  • 哇!这听起来很奇怪。非常好的思路。让我在这一刻犹豫不决,但我要让它坐一会儿,想想后果。非常感谢您的回答。
【解决方案4】:

CGI 救援!

如果您创建了一个 cgi-bin 目录并进行了适当的配置,您将通过 stdin 收到消息(据我所知,文件根本不会以这种方式写入磁盘)。

所以,在你的 apache 配置中添加

ScriptAlias /cgi-bin/ /var/www/<site-dir>/cgi-bin/
<Directory "/var/www/<site-dir>/cgi-bin">
    AllowOverride None
    Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
    Order allow,deny
    Allow from all
</Directory>

然后编写一个 CGI 模式的 PHP 脚本来解析 post 数据。根据我的(有限)测试,似乎没有创建本地文件。该示例转储了从标准输入读取的内容以及环境变量,以便您了解可以使用的内容。

示例脚本安装为 /var/www//cgi-bin/test

#!/usr/bin/php
Content-type: text/html

<html><body>
<form enctype="multipart/form-data" action="/cgi-bin/test" method="POST">
    <!-- MAX_FILE_SIZE must precede the file input field -->
    <input type="hidden" name="MAX_FILE_SIZE" value="30000" />
    <!-- Name of input element determines name in $_FILES array -->
    Send this file: <input name="userfile" type="file" />
      <input type="submit" value="Send File" />
</form>
<pre>
<?
echo "\nRequest body\n\n";
$handle = fopen ("php://stdin","r");
while (($line = fgets($handle))) echo "$line";
fclose($handle);
echo "\n\n";
phpinfo(INFO_ENVIRONMENT);
echo "\n\n";
?>
</pre>
</body></html>

示例输出 这是我上传纯文本文件时的输出(源):

<html><body>
<form enctype="multipart/form-data" action="/cgi-bin/test" method="POST">
    <!-- MAX_FILE_SIZE must precede the file input field -->
        <input type="hidden" name="MAX_FILE_SIZE" value="30000" />
        <!-- Name of input element determines name in $_FILES array -->
        Send this file: <input name="userfile" type="file" />
          <input type="submit" value="Send File" />
</form>
<pre>

Request body

-----------------------------19908123511077915841334811274
Content-Disposition: form-data; name="MAX_FILE_SIZE"

30000
-----------------------------19908123511077915841334811274
Content-Disposition: form-data; name="userfile"; filename="uploadtest.txt"
Content-Type: text/plain

This is some sample text

-----------------------------19908123511077915841334811274--


phpinfo()

Environment

Variable => Value
HTTP_HOST => dev.squello.com
HTTP_USER_AGENT => Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.2.16) Gecko/20110323 Ubuntu/10.04 (lucid) Firefox/3.6.16
HTTP_ACCEPT => text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
HTTP_ACCEPT_LANGUAGE => en-gb,en;q=0.5
HTTP_ACCEPT_ENCODING => gzip,deflate
HTTP_ACCEPT_CHARSET => ISO-8859-1,utf-8;q=0.7,*;q=0.7
HTTP_KEEP_ALIVE => 115
HTTP_CONNECTION => keep-alive
HTTP_REFERER => http://dev.squello.com/cgi-bin/test
CONTENT_TYPE => multipart/form-data; boundary=---------------------------19908123511077915841334811274
CONTENT_LENGTH => 376
PATH => /usr/local/bin:/usr/bin:/bin
SERVER_SIGNATURE => <address>Apache/2.2.14 (Ubuntu) Server at dev.squello.com Port 80</address>

SERVER_SOFTWARE => Apache/2.2.14 (Ubuntu)
SERVER_NAME => dev.squello.com
SERVER_ADDR => 127.0.0.1
SERVER_PORT => 80
REMOTE_ADDR => 127.0.0.1
DOCUMENT_ROOT => /var/www/dev.squello.com/www
SERVER_ADMIN => webmaster@localhost
SCRIPT_FILENAME => /var/www/dev.squello.com/cgi-bin/test
REMOTE_PORT => 58012
GATEWAY_INTERFACE => CGI/1.1
SERVER_PROTOCOL => HTTP/1.1
REQUEST_METHOD => POST
QUERY_STRING =>  
REQUEST_URI => /cgi-bin/test
SCRIPT_NAME => /cgi-bin/test


</pre>
</body></html>

【讨论】:

  • o_O 如果属实,这听起来几乎是无限令人兴奋的。我不确定 CGI 模式 PHP 是否适合我们(我怀疑不是),但这是一条完全不同的途径。绝对必须调查这种“不寻常”的行为。非常感谢!
  • 大概是安全约束阻止您讨论安全约束...如果不是,CGI 的对象是什么?
  • 我想我可能敢这么精确,实际上! :) 但我不认为我可以不让它成为一个更大的宣传。这里提到的最琐碎和最快的就是性能。然后我被引用了 CGI 处理程序的一些安全问题,我不知道这些问题(不是我的专业领域)。但!你会注意到我没有编辑我的问题来取消 CGI-PHP 的资格,我也不打算这样做。这是我目前看到的最优雅的解决方案。 (虽然我们可能会修补 PHP,但我不会向其他正在寻找这个问题的答案的人推荐它。)
  • @downvoter 我对没有临时文件有误吗?我不希望这段代码给任何人造成安全问题! (通常 CGI 安全性超出范围)
  • +1 用于手动输入处理。防止创建 PHP 的自动 $_FILES 数组可能是唯一使临时文件远离文件系统的可行方法。
【解决方案5】:

您可以创建一个tmpfs 并使用适当的umask 安装它。这样,唯一可以从中读取文件的进程是创建它的用户。而且,因为这是一个tmpfs,所以磁盘上不会存储任何内容。

我建议反对 ADW 的encfs 解决方案。 Encfs 不加密卷,但它会逐个文件加密文件,仍然会暴露大量元数据。

【讨论】:

  • '没有任何东西存储在磁盘上' - 问题不在于磁盘,但问题在于它在文件系统中。 '正确的 umasks' 充其量是每个程序(PHP / Apache)的解决方案,而我正在寻找一个每个请求的解决方案。但是非常感谢encfs 的提醒!赞赏。
  • 正确的 umask 也可以是每个用户的解决方案。只需 umask 以便上传的文件属于特定用户并具有权限 600 例如。然后让 PHP-FPM 处理每个虚拟主机作为不同用户运行。
  • 啊,我明白你在想什么!我们没有使用多个虚拟主机。这不是一个共享服务器设置,它是一个单一的高安全性 Web 应用程序。机密性是针对最终用户的,因此,如果系统随时受到威胁,您最终不会在临时文件系统中拥有潜在的重要客户端文件。
【解决方案6】:

您是否考虑过在用户和网络服务器之间放置一个层?使用perlbal之类的东西和一些自定义代码在网络服务器前面可以让你在上传文件被写入任何地方之前拦截它们,加密它们,将它们写入本地ramdisk,然后在网络服务器上代理请求(以及文件的文件名和解密密钥)。

如果 PHP 进程崩溃,加密文件会被保留但无法解密。没有未加密的数据被写入(ram)磁盘。

【讨论】:

  • Hawt。谢谢,这与我最初设想的解决方案相当接近,但我一生都无法弄清楚如果不完全从头开始编写东西该怎么做。 “反向代理”是我没有想到的关键字。非常感谢 Perlbal 链接!
  • 类似的方法是使用 mod_ext_filter(随 HTTPd 提供)。它能够将请求正文发送到命令行程序,然后该程序返回(修改后的)正文以进行进一步的请求处理。在外部程序中,您可以解析请求部分,生成一个长加密密钥(对称应该没问题),用它加密文件内容部分并将加密密钥作为请求变量添加到请求正文。在 PHP 中,您使用该密钥解密上传的文件。这些文件仍将由 PHP 存储在磁盘上,但您将使用几乎随机的长密钥进行加密。
  • 我接受了这个作为我的答案,因为它可能是我实际问题的最直观的解决方案。 :) (我想我会把接受和赏金分开,因为这里有很多伟大而有见地的答案。)谢谢你的回答!
【解决方案7】:

在您的脚本有机会截获数据之前,PHP 会将上传的文件存储到文件系统中,在这种情况下,$HTTP_RAW_POST_DATA 将为空(即使您设置了file_uploads = Off)。

对于小文件,您可以尝试设置&lt;form enctype="application/x-www-form-urlencoded" ...,但我没有成功使用它。我建议您重新编译 php 并注释掉处理文件上传的部分,如 Bug report(由 pollita@php.net 评论)。

专业版:根本没有文件上传,数据在 php://input 缺点:重新编译,没有 Vendor 支持

【讨论】:

  • 是的,您的第一段正是我要解决的问题。不过,这确实是一颗宝石!感谢您提供错误报告链接!这提供了一些惊人的见解。 (我从没想过将事情重新路由到 php://input 会那么“容易”!将不得不玩这个有点......)
【解决方案8】:

您的担忧是有道理的。这个问题有几个解决方案。一种是将文件存储在数据库中。像 MongoDB 或 CouchDB 这样的 NoSQL 数据库旨在有效地存储文件。 MySQL 是另一种选择,它比 NoSQL 具有优势。像 MySQL 这样的关系数据库可以很容易地植入访问控制,因为您可以通过主键关联 filesusers 表。

在 MySQL 中,您可以使用 longblob 数据类型保存 2^32 位或大约 ~500mb。您可以使用 MEMORY 引擎创建驻留在内存中的表:CREATE TABLE files ENGINE=MEMORY ...。此外。 MySQL 确实有aes_encrypt()des_encrypt() 形式的加密,但它们都使用ECB mode which is garbage

$sensi_file=file_get_contents($_FILES['sensitive']['tmp_name']);
unlink($_FILES['sensitive']['tmp_name']);//delete the sensitive file. 
$sensi_file=mysql_real_escape_string($sensi_file);//Parametrized quires will also use this function so that should also be binary safe.
mysql_query("insert into files (file)values('$sensi_file')"); 

只需选择文件并像使用$sensi_file 一样使用它。请记住,您正在转义输入以获取字符文字,从而存储原始二进制文件。

【讨论】:

  • 这是我以前从未见过的一般文件处理方面的一些很棒的东西,但是...$_FILES['sensitive']['tmp_name'] 是我的问题 - 文件已经在文件系统中,PHP 把它放在那里.我一开始就不希望它存在(至少未加密)。如果 PHP 进程继续按预期进行,那么我们已经在按照我们的意愿处理数据了。不过,如果发生崩溃,我们依赖进程外清理。
  • (在这一点上,我几乎完全确定没有比“使用 SELinux/comparable”或“fork PHP”或“不使用 PHP”更好的解决方案,但我喜欢希望!)
  • @pinkgothic 文件上传的行为由 PHP 控制,用 C++ 编写。我建议不要直接弄乱代码,尽管这是一种选择。您可以设置一个小型 ram 磁盘并将其设置为 tmp 上传文件的目的地。
  • "文件上传的行为由 PHP 控制,并且是用 C++ 编写的。" 我知道。 :) 不过,RAM 磁盘并不能解决安全问题。不过,非常感谢您的见解!
【解决方案9】:

您是否考虑过使用 FUSE 创建一个只能由特定用户访问的加密目录?

http://www.arg0.net/encfs

内存不会与特定进程相关联,但文件只能由特定用户访问(与您的网络服务器运行相同的用户!)这可能就足够了吗?

【讨论】:

  • "这些文件只能由特定用户访问" - 这听起来与 SELinux 提供的内容密切相关(请参阅 cmets @plasma87 的回答),而且绝对是我们要追求。感谢您的链接!
  • 请注意,encfs 会逐个文件加密文件。这不是卷加密。尽管实际文件内容是加密的,但这仍然会保留元数据。
【解决方案10】:

您是否考虑过在 Linux 下创建 RAMdisk?

http://www.vanemery.com/Linux/Ramdisk/ramdisk.html

由于这将显示为本地文件系统位置,因此您只需将 PHP 实例(假设它具有正确的权限)指向该位置。

我不确定如果磁盘发生故障会产生什么后果,我想该位置会变得不可写或不存在。性能和大文件可能会产生进一步的影响,但由于这不是我的专业领域,我不能告诉你太多。

希望对你有所帮助。

【讨论】:

  • 感谢您抽出宝贵时间回复,但是……恐怕不会。我要解决的问题是,如果进程崩溃(或者根本没有,真的),对临时文件的未加密“第三方进程”访问。据我了解,RAMdisk 无法解决这个问题 - 内存与文件系统相关联,而不是与进程相关联。您仍然有未加密的临时文件,它们只是没有存储在磁盘上。 [你会注意到我在我的问题中使用了“文件系统”这个词,而不是“磁盘”——这是故意的。]
  • 一个脚注 - 那篇介绍性教程文章的作者指出,当 RAMdisk 被销毁时,没有剩余的人工制品。但是,我建议清除您的 Apache 日志,因为这些日志可能包含访问跟踪等。
  • 抱歉,我没有仔细阅读这个问题。可以动态加密文件并将它们存储在临时磁盘中,但是如果您的吞吐量非常高,您可能会发现您的服务器会受到影响。使用严格的文件系统权限和安全守护程序(例如 SELinux)的组合来阻止对 Apache + PHP 之外的任何进程/用户的访问是否足够?
  • 实际上,我们正在研究 SELinux,所以这绝对是一个选择,但它是我们不希望将其作为临时文件的唯一安全措施之一。不过,动态加密文件将是完美的——性能并不是我们真正关心的问题。如何让 PHP 做到这一点? (旁注,关于您之前的评论:我们的日志已得到妥善处理,但这是对其他人的宝贵建议,因此感谢您将其放在这里。)
  • 如果您通过 POST 使用文件上传,默认情况下您的文件将被写入磁盘(通常是 /tmp)。我的解决方案并非完全“即时”(即在进行中),但它可能适合您的需求。您可以尝试将 upload_tmp_dir (php.ini) 修改为 RAMdisk 的位置。完成此操作后,对 GPG 进行上传后调用。我敢肯定有一些关于这样做的帖子,但为了方便起见,这里有一个关于在 devzone 上执行此操作的快速教程 [链接]devzone.zend.com/article/1265 这又回到了我之前提到的关于开销和对系统二进制文件的依赖的内容.
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-10-07
  • 1970-01-01
  • 2014-06-22
  • 1970-01-01
  • 1970-01-01
  • 2021-03-13
  • 1970-01-01
相关资源
最近更新 更多