如何在 PHP 中检查不完整的 POST 请求

Posted

技术标签:

【中文标题】如何在 PHP 中检查不完整的 POST 请求【英文标题】:How to check for incomplete POST request in PHP 【发布时间】:2013-12-13 20:36:29 【问题描述】:

当连接缓慢的远程 Web 客户端无法发送带有 multipart/form-data 内容的完整 POST 请求但 php 仍使用部分接收的数据来填充 $_POST 数组时,我遇到了问题。因此,$_POST 数组中的一个值可能不完整,并且可能会丢失更多值。我首先尝试在Apache list 中提出相同的问题,并得到an answer Apache 不缓冲请求正文并将其作为 巨大的斑点。

这是我的示例 POST 请求:

POST /test.php HTTP/1.0
Connection: close
Content-Length: 10000
Content-Type: multipart/form-data; boundary=ABCDEF

--ABCDEF
Content-Disposition: form-data; name="a"

A
--ABCDEF

您可以看到 Content-Length10000 字节,但我只发送了一个 var a=A

PHP 脚本是:

<?php print_r($_REQUEST); ?>

Web 服务器等待我的其余请求大约 10 秒(但我不发送任何内容),然后返回此响应:

HTTP/1.1 200 OK
Date: Wed, 27 Nov 2013 19:42:20 GMT
Server: Apache/2.2.22 (Debian)
X-Powered-By: PHP/5.4.4-14+deb7u3
Vary: Accept-Encoding
Content-Length: 23
Connection: close
Content-Type: text/html

Array
(
     [a] => A
)

所以这是我的问题:如何在 PHP 中验证发布请求是否已完全收到? $_SERVER['CONTENT_LENGTH'] 会在请求头中显示 10000,但是有没有办法检查收到的真实内容长度?

【问题讨论】:

suhosin 会是你的答案吗? ***.com/a/8451656/1001641 @Naveed 来自Suhosin feature list 似乎他们只支持对变量名/值长度的限制,但这不是我需要的。我需要验证 Content-Length 是否与收到的真实消息正文的大小匹配。 远程客户端实际上是一个带有HTML页面的浏览器? @MeNa 不,这是一个自定义应用程序,我可以对其进行修改。 【参考方案1】:

这是 PHP 中的一个已知错误,需要在那里修复 - https://bugs.php.net/bug.php?id=61471

【讨论】:

虽然链接可以回答问题,但最好在答案中包含其内容的所有相关部分,并将链接仅作为参考...【参考方案2】:

如果计算内容长度不合理,您可能可以对客户端发送的数据进行签名。

使用 javascript,在提交之前以合理的方式(即根据需要对其进行排序)将表单数据序列化为 json 字符串或等效项。使用一种或两种相当快速的算法(例如 crc32、md5、sha1)对这个字符串进行哈希处理,并将这些额外的哈希数据添加到即将作为签名发送的内容中。

在服务器上,从 $_POST 请求中去除这些额外的哈希数据,然后在 PHP 中重做相同的工作。相应地比较散列:如果散列匹配,翻译中不会丢失任何内容。 (如果您想避免误报的微小风险,请使用两个哈希值。)

我敢打赌,有一种合理的方法可以对文件执行类似的操作,例如在 JS 中获取它们的名称和大小,并将该附加信息添加到已签名的数据中。

这与某些 PHP 框架为避免篡改会话数据所做的工作有些相关,当后者被管理并存储在客户端 cookie 中时,因此您可能会在后一种情况下找到一些现成的代码来执行此操作.


原答案:

据我所知,发送 GET 或 POST 请求与发送类似以下内容的金额之间的区别或多或少:

GET /script.php?var1=foo&var2=bar
headers

vs 发送类似的东西:

POST /script.php
headers

var1=foo&var2=bar              <— content length is the length of this chunk

因此,对于每个部分,您可以计算长度并检查它与 content-length 标头所宣传的长度。

$_FILES 条目有一个方便的大小字段,您可以直接使用。 对于$_POST 数据,重建发送的查询字符串并计算其长度。

注意事项:

    您需要知道在某些情况下数据的预期发送方式,例如var[]=foo&amp;var[]=bazvar[0]=foo&amp;var[1]=baz 在后一种情况下,您处理的是 C 字符串长度而不是多字节长度。 (不过,如果得知奇怪的浏览器在各处表现不一致,我也不会感到惊讶。)

进一步阅读:

http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13 http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4

【讨论】:

我使用的是multipart/form-data,而不是application/x-www-form-urlencoded 是的。据我所知,POST 部分的格式都相同(在边界分隔符之间),以及每个单独部分的标题。 我已经评论了另一个关于在 PHP 中为收到的请求计算 Content-Length 的答案。您需要记住将所有多部分边界计算为Content-Length 的一部分。也可能有您在 PHP 中没有收到的任意大小的序言/尾声。 我预计边界和其中的标题与大小无关。但我想这取决于浏览器供应商和服务器的解释。另外:无论长度如何计算,由于httpd.apache.org/docs/2.0/misc/…,您可能仍然会遇到问题 签名请求的想法或简单来说计算哈希并将其发送到其他答案中至少已经提到了 2 次。【参考方案3】:

其他一些可能有用的解决方案... 如果对方的连接速度很慢,只需取消执行帖子的限制即可。

set_time_limit(0);

而且你会确定将发送孔柱数据。

【讨论】:

除非用户在处理请求时中止请求。【参考方案4】:

我认为不可能从 $_REQUEST 超全局计算原始内容大小,至少对于多部分/表单数据请求而言。

我会在您的 http 请求中添加一个自定义标头,其中包含所有参数 = 值哈希,以便在服务器端进行检查。标头肯定会到达,因此您的哈希标头始终存在。一定要以相同的顺序加入参数,否则hash会不同。还要注意编码,客户端和服务端必须一致。

如果您可以配置 Apache,您可以使用 mod_proxy 添加一个 vhost,配置为在同一服务器上的另一个 vhost 上代理。这应该过滤不完整的请求。请注意,这样会浪费每个请求 2 个套接字,因此如果您想这样做,请注意资源使用情况。

【讨论】:

【参考方案5】:

如果可以把enctype改成

multipart/form-data-alternate

你可以检查

strlen(file_get_contents('php://input'))

对比

$_SERVER['CONTENT_LENGTH']

【讨论】:

是不是说我需要在PHP中自己解析multipart?【参考方案6】:

我认为您正在寻找的是 $HTTP_RAW_POST_DATA,这将为您提供 真实 POST 长度,然后您可以将其与 $_SERVER['CONTENT_LENGTH' ].

【讨论】:

引用here:$HTTP_RAW_POST_DATA 不适用于enctype="multipart/form-data"【参考方案7】:

据我所知,在将multipart/form-data 用作Content-Type 时,无法检查接收到的内容的大小是否与Content-Length 标头的值匹配,因为您无法获取原始内容。

1) 如果您可以更改Content-Type(例如更改为application/x-www-form-urlencoded),您可以读取php://input,其中将包含请求的原始内容。 php://input 的大小应该与Content-Length 匹配(假设Content-Length 的值是正确的)。如果匹配,您仍然可以使用$_POST 来获取处理后的内容(常规帖子数据)。阅读php://inputhere。

2) 或者你可以在客户端序列化数据并发送为text/plain。服务器可以使用与上述相同的方式检查大小。服务器将需要反序列化接收到的内容才能使用它。如果客户端生成序列化数据的散列并将其发送到标头中(例如X-Content-Hash),服务器也可以生成散列并检查它是否与标头中的匹配。您无需检查哈希,并且可以 100% 确定内容正确。

3) 如果您无法更改Content-Type,则需要与大小不同的内容来验证内容。客户端可以使用额外的标头(类似于X-Form-Data-Fields)来总结您发送的内容的字段/键/名称。然后,服务器可以检查标题中提到的所有字段是否都存在于内容中。

4) 另一种解决方案是让客户端将预定义的键/值作为内容中的 last 条目。比如:

--boundary
Content-Disposition: form-data; name="_final_field_"

TRUE
--boundary--

服务器可以检查该字段是否存在于内容中,如果存在,则内容必须是完整的。

更新

当需要传递二进制数据时,不能使用选项1,但仍然可以使用选项2:

客户端可以base64 编码二进制条目,序列化数据(使用您喜欢的任何技术),生成序列化数据的散列,将散列作为标头发送,将数据作为主体发送。 服务器可以生成接收到的内容的散列,检查与标头中的散列(并报告不匹配),反序列化内容,base64 解码二进制条目。

这比简单地使用multipart/form-data 要多一些工作,但服务器可以100% 保证验证内容与客户端发送的内容相同。

【讨论】:

我传递了一些二进制数据,所以我不想使用application/x-www-form-urlencoded,因为它会增加数据大小。我喜欢使用自定义标头的想法,我认为您的解决方案可以通过在标头中传递所有参数名称和值的总长度并使用$_REQUEST$_POST 在 PHP 中检查它来简化。至于加个_final_field_,MeNo 已经提过这个想法了。 是的,使用二进制数据选项 1 不行。我已经相应地更新了我的答案。【参考方案8】:

我还建议使用 hidden 值,或者像 MeNa 提到的散列。 (问题在于某些算法在平台上的实现方式不同,因此您在 js 中的 CRC32 可能与在 PHP 中的 CRC32 不同。但是通过一些测试,您应该能够找到兼容的)

我将建议使用对称加密,只是因为它是一种选择。 (我不相信它比散列更快)。 除了机密性之外,加密还提供完整性,即。收到的消息是发送的消息吗?

虽然流密码非常快,但块密码(如 AES)也可以非常快,但这取决于您的系统、您使用的语言等(同样在这里,不同的实现意味着并非所有加密都是平等的)

如果您无法解密消息(或者它给出了乱码),那么消息就是不完整的。

但是说真的,请使用散列。散列客户端上的 POST,首先检查服务器上散列的长度。 (一些?)哈希是固定长度的,所以如果长度不匹配,那就错了。然后散列收到的 POST 并与 POST 散列进行比较。 如果您在完整的 POST 中执行此操作,则以指定的顺序(因此任何重新排序都被撤消)开销是最小的。

所有这一切,假设您只是无法检查帖子消息以查看是否缺少字段并且 is_set==True, length > 0 , !empty()...

【讨论】:

【参考方案9】:

也许你可以检查一个有效的变量,但不能检查长度,例如:

// client
$clientVars = array('var1' => 'val1', 'otherVar' => 'some value');
ksort($clientVars);  // dictionary sorted
$validVar = md5(implode('', $clientVars));
$values = 'var1=val1&otherVar=some value&validVar=' . $validVar;
httpRequest($url, values);

// server
$validVar = $_POST['validVar'];
unset($_POST['validVar']);
ksort($_POST);  // dictionary sorted
if (md5(implode('', $_POST)) == $validVar) 
    // completed POST, do something
 else 
    // not completed POST, log error and do something

【讨论】:

【参考方案10】:

关于由于连接问题而完全丢失的表单值,您可以检查它们是否已设置:

if(isset($_POST['key'])
    //value is set
else
    //connection was interrupted

对于较大的表单数据(例如图片上传),您可以使用检查接收文件的大小

$_FILES['key']['size']

一个简单的解决方案可能是使用 JavaScript 在客户端计算文件大小,并将该值附加到表单作为表单提交时的隐藏输入。您可以使用

之类的方法在 JS 中获取文件大小
var filesize = input.files[0].size;

参考:JavaScript file upload size validation

然后在文件上传时,如果隐藏表单输入的值与上传文件的大小匹配,则请求不会因网络连接问题而中断。

【讨论】:

您不能以这种方式验证内容长度。首先,您需要计算所有多部分边界的大小。此外,multipart 可以有任何大小的序言/尾声,这些内容长度计入内容长度,但您在 PHP 中没有收到。 顺便说一句,标题不是内容的一部分。 如果 PHP 无法访问多部分边界前导或尾声的大小,则无法通过将 Content-Length 请求标头与接收到的数据进行比较,作为纯 PHP 解决方案来完成。正如@MeNa 和我都建议的那样,您需要一个额外的字段或某种参数来跟踪传输的帖子数据的大小。【参考方案11】:

我猜远程客户端实际上是一个带有 HTML 页面的浏览器。 否则,请告诉我,我会尝试调整我的解决方案。

您可以添加字段 &lt;input type="hidden" name="complete"&gt;(例如)作为 last 参数。在PHP 首先检查这个参数是否是从客户端发送的。如果发送了这个参数 - 你可以确定你得到了整个数据。

现在,我不确定是否必须根据 RFC(HTML 和 HTTP)保留参数的顺序。但我尝试了一些变化,我发现订单确实保持不变。

更好的解决方案是,计算(在客户端)参数的哈希并将他作为另一个参数发送。所以你可以绝对确定你得到了全部数据。但这听起来有点复杂......

【讨论】:

这也是我的想法——最后再添加一个参数。但是我担心中间的某个代理是否可以以不同的顺序重新组合请求。我想知道我是否可以在 PHP 中收到真正的 POST 大小。 我回答的最后一部分呢?也发送参数的哈希值,然后在您的服务器上再次计算哈希值并进行比较。 我认为这是我能做的最好的了,但我想再等一会儿以获得更好的解决方案。【参考方案12】:

它们可能会受到 Apache 或 PHP 的限制。我相信 Apache 也有一个配置变量。

这里是 PHP 设置;

php.ini

post_max_size=20M
upload_max_filesize=20M

.htaccess

php_value post_max_size 20M
php_value upload_max_filesize 20M

【讨论】:

这与大小限制无关。 POST 大小只有几千字节。这里的问题是,如果在发布请求时连接丢失,PHP 仍然会解释部分接收到的数据,无论 Content-Length 不匹配。

以上是关于如何在 PHP 中检查不完整的 POST 请求的主要内容,如果未能解决你的问题,请参考以下文章

如何在 AWS ELB 日志中获取完整的 POST 正文?

如何在 PHP 中检查没有密钥的 JWT 完整性?

如何检查请求POST中是不是存在值? [复制]

php如何发送带中文的http请求?

如何在 C++ 中使用 wininet 创建 POST 请求

检查请求是不是在 PHP 中回发 [重复]