php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Doc Bug #69322 LimitRequestBody excession not handled correctly
Submitted: 2015-03-28 22:56 UTC Modified: 2021-10-18 13:33 UTC
Votes:2
Avg. Score:4.0 ± 1.0
Reproduced:2 of 2 (100.0%)
Same Version:0 (0.0%)
Same OS:1 (50.0%)
From: gmoniker at gmail dot com Assigned:
Status: Verified Package: Apache2 related
PHP Version: 7.4.3 OS: Ubuntu 20.04
Private report: No CVE-ID: None
 [2015-03-28 22:56 UTC] gmoniker at gmail dot com
Description:
------------
If you set a LimitRequestBody in the Apache configuration there are unexpected results with the apache2 PHP module.

Expected: an excession of the LimitRequestBody should cancel the entire handling of a PHP request. The script called should not run, and thus produce no output. A PHP script configured to handle a 413 Error should not accept the request body  but be fully functional otherwise.

There are up to four parameters to this bug:
1. The original handler in use (The one Apache selects or first accepts for handling the URI requested by the client.)
2. The ErrorDocument for 413. (Is it PHP or not)
3. The Apache version.
4. Whether the request is the first to PHP on a worker.

In Apache 2.2 (with PHP 5.3.10):
When you call a static html file handled by the core module, there is only the output of the 413 ErrorDocument. Using PHP for the ErrorDocument works correctly.
When you call a PHP script, you get first the output of the ErrorDocument and then the output of the original PHP script. Using PHP for the ErrorDocument works correctly.

In Apache 2.4 (with PHP 5.5.9)
When you call a static html file handled by the core module, there is only the output of the 413 ErrorDocument. Using PHP for the ErrorDocument works correctly.
When you call a PHP script, you get first the output of the ErrorDocument and then the output of the original PHP script. If you call this as the very first PHP call on a worker, the worker crashes with segfault.

All tests done with apache prefork mpm and opcache off on 64-bit system.

Test script:
---------------
Needed: Apache with prefork mpm, PHP module for Apache, curl, 64-bit system. For 32-bit system results unknown.

Set in Apache config:
LimitRequestBody 1000
ErrorDocument 413 "/error413.php"

In PHP config for Apache:
disable opcache
;zend_extension=opcache.so

Put a PHP script posterror.php with some output, an error413.php script with some output, and index.html in a suitable documentroot.

Stop pre-running Apache webserver
Start Apache in different terminal: apache2ctl -X

printf '%*s' 2000   | sed 's/ /-/g' > body.txt
curl -si -F "post=@body.txt;filename=body.txt" http://localhost/posterror.php
curl -si -F "post=@body.txt;filename=body.txt" http://localhost/index.html


Expected result:
----------------
When Apache intercepts the call to a script with a 413 error, I expect the PHP script not to run and thus produce no output, only the output of the ErrorDocument if it exists together with a 413 Request Entity Too Large response status.

In the case of Apache 2.4, I do not expect a difference between the first call to PHP on a running Apache worker and subsequent calls.

Actual result:
--------------
Backtrace for segfault on first request to Apache 2.4 with a PHP script and a PHP ErrorDocument for 413:

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff3a086cc in php_session_rfc1867_callback (event=5, event_data=0x7fffffffc700, extra=0x7fffffffc5d0)
    at /root/php5-5.5.9+dfsg/ext/session/session.c:2747
2747                            if (Z_TYPE(progress->sid) && progress->key.c) {
(gdb) bt
#0  0x00007ffff3a086cc in php_session_rfc1867_callback (event=5, event_data=0x7fffffffc700,
    extra=0x7fffffffc5d0) at /root/php5-5.5.9+dfsg/ext/session/session.c:2747
#1  0x00007ffff3b8df6e in rfc1867_post_handler (content_type_dup=0x7ffff7f26ff8 "\252~M\353R\006",
    arg=0x7ffff7f29680) at /root/php5-5.5.9+dfsg/main/rfc1867.c:1260
#2  0x00007ffff3b88b73 in sapi_handle_post (arg=0x7ffff7f29680) at /root/php5-5.5.9+dfsg/main/SAPI.c:189
#3  0x00007ffff3b90217 in php_default_treat_data (arg=0, str=0x0, destArray=0x0)
    at /root/php5-5.5.9+dfsg/main/php_variables.c:322
#4  0x00007ffff39a1a48 in mbstr_treat_data (arg=0, str=0x0, destArray=0x0)
    at /root/php5-5.5.9+dfsg/ext/mbstring/mb_gpc.c:69
#5  0x00007ffff3b913e2 in php_auto_globals_create_post (name=0x7ffff13474a0 "_POST", name_len=5)
    at /root/php5-5.5.9+dfsg/main/php_variables.c:665
#6  0x00007ffff3bf3d68 in zend_auto_global_init (auto_global=0x555555818480)
    at /root/php5-5.5.9+dfsg/Zend/zend_compile.c:6724
#7  0x00007ffff3c1f728 in zend_hash_apply (ht=0x555555803d80,
    apply_func=0x7ffff3bf3d1c <zend_auto_global_init>) at /root/php5-5.5.9+dfsg/Zend/zend_hash.c:716
#8  0x00007ffff3bf3da1 in zend_activate_auto_globals () at /root/php5-5.5.9+dfsg/Zend/zend_compile.c:6734
#9  0x00007ffff3b91179 in php_hash_environment () at /root/php5-5.5.9+dfsg/main/php_variables.c:625
#10 0x00007ffff3b7d34d in php_request_startup () at /root/php5-5.5.9+dfsg/main/main.c:1595
#11 0x00007ffff3d50fd7 in php_apache_request_ctor (r=0x7ffff7e8a0a0, ctx=0x7ffff7e864d0)
    at /root/php5-5.5.9+dfsg/sapi/apache2handler/sapi_apache2.c:502
#12 0x00007ffff3d515f8 in php_handler (r=0x7ffff7e8a0a0)
    at /root/php5-5.5.9+dfsg/sapi/apache2handler/sapi_apache2.c:618
#13 0x00005555555aa830 in ap_run_handler (r=0x7ffff7e8a0a0) at config.c:169
#14 0x00005555555aad79 in ap_invoke_handler (r=r@entry=0x7ffff7e8a0a0) at config.c:439
#15 0x00005555555c033a in ap_process_async_request (r=0x7ffff7e8a0a0) at http_request.c:317
#16 0x00005555555c0614 in ap_process_request (r=r@entry=0x7ffff7e8a0a0) at http_request.c:363
#17 0x00005555555bd0b2 in ap_process_http_sync_connection (c=0x7ffff7e8e290) at http_core.c:190
#18 ap_process_http_connection (c=0x7ffff7e8e290) at http_core.c:231
#19 0x00005555555b3e70 in ap_run_process_connection (c=0x7ffff7e8e290) at connection.c:41
#20 0x00005555555b4258 in ap_process_connection (c=c@entry=0x7ffff7e8e290, csd=<optimized out>)
    at connection.c:202
#21 0x00007ffff468a767 in child_main (child_num_arg=child_num_arg@entry=0) at prefork.c:704
#22 0x00007ffff468a96c in make_child (s=0x7ffff7fc1de0, slot=slot@entry=0) at prefork.c:746
#23 0x00007ffff468b6b1 in prefork_run (_pconf=<optimized out>, plog=0x7ffff7fbd028, s=0x7ffff7fc1de0)
    at prefork.c:956
#24 0x00005555555916de in ap_run_mpm (pconf=0x7ffff7ff0028, plog=0x7ffff7fbd028, s=0x7ffff7fc1de0)
    at mpm_common.c:96
#25 0x000055555558ae76 in main (argc=2, argv=0x7fffffffe658) at main.c:777


Patches

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2021-10-07 11:39 UTC] cmb@php.net
-Status: Open +Status: Feedback -Assigned To: +Assigned To: cmb
 [2021-10-07 11:39 UTC] cmb@php.net
Is this still an issue with any of the actively supported PHP
versions[1]?

[1] <https://www.php.net/supported-versions.php>
 [2021-10-08 10:09 UTC] gmoniker at gmail dot com
-Status: Feedback +Status: Assigned -Operating System: Ubuntu 14.04 +Operating System: Ubuntu 20.04 -PHP Version: 5.6.7 +PHP Version: 7.4.3
 [2021-10-08 10:09 UTC] gmoniker at gmail dot com
I have done some testing with PHP 7.4 on Ubuntu 20 and Apache SAPI PHP module.

The segfaulting Apache problem has gone away apparently, couldn't reproduce that. So having a PHP errordocument for status 413 Entity Too Large is no longer relevant.

The possibly problematic behaviour occurs when the Apache LimitRequestBody size is below the PHP max_post_size setting. When an incoming POST has a size above LimitRequestBody but under max_post_size, then the called PHP script just runs but with empty POST and FILES globals, but with incoming CONTENT_LENGTH set to the original POST size.

When the incoming POST size is above the PHP max_post_size, then the script does not run.

If you compare this with the Apache static html handler, then that only serves the setup 413 errordocument when a POST exceeds a LimitRequestBody and not the called html file.
 [2021-10-08 11:01 UTC] cmb@php.net
Thanks for the swift reply, and for checking again!  I'll have a
closer look ASAP.
 [2021-10-10 14:12 UTC] gmoniker at gmail dot com
It is probably risky to change "documented" behaviour.

https://stackoverflow.com/questions/2133652/how-to-gracefully-handle-files-that-exceed-phps-post-max-size

The current approach is more or less, don't populate $_POST if it exceeds the max post size, but execute the script anyway. It is not surprising perhaps that an input filter setting the status to 413 on reading the body data is sort of treated the same way. Although in the case of Entity Too Large on the body, the output is completely unpredictable because the 413 ErrorDocument output (default HTML or custom 413) is prefixed to the script output, so that is really messy.

What is the important lesson to take away I think, is that using PHP in input filters (Apache body request limit) or probably output filter as well, when you are also executing PHP as a main handler script of the request is fraught with dangers and generally not a *good* idea. Even just having a LimitRequestBody setting with PHP module active and a default 413 html, makes for strange output documents when that limit is hit, even if that leaves the server running fine.

I did years ago look into what would be necessary to fix that, but that is a major rewrite of the Apache SAPI code. See my repo: https://github.com/gmoniker/php-apache2handler/commits/master

The immediate catalyst in that effort was a pipeline bug with Apache 2.4 https://bugs.php.net/bug.php?id=68486

With a little more stress testing with a 413 PHP ErrorDocument I have been able now to also get Apache to segfault with PHP 7.4 SAPI. This happens when you turn off enable_post_data_reading or eliminate the POST(P) from variables_order and then making a LimitRequestBody exceeding non-chunked (content-length set) request. The request is served, but the next PHP request segfaults the server.

Even with enable_post_data_reading and variables_order on their default, the output gets closed before the 413 ErrorDocument is served, and the environment in which it runs is not really dependable, showing such things as writing to the Apache stdout and leaving zombies if you execute a shell command, so even with default settings running with a 413 ErrorDocument in PHP and Apache module both is not safe for any production. The problem stems from the ErrorDocument running in an already initialized or destructed PHP context.

There are also some strange quirks with chunked requests. You can run into the 413 body limit and then have an empty $_POST but still read more bytes from php://stdin then the value of the body limit would indicate to be allowed, because the SAPI has already read more of the body before processing the limit from Apache.

Another quirk I found, if you have chunked body data coming in, and there is pause of about 10 seconds between data chunks, then the PHP script will start executing with an empty $_POST also, and no data on php://stdin. So the known workaround to detect a max_post_size exceeded in PHP script will also detect this case of paused data transfer without exceeding the max post size.

Maybe the solution would be more of the documenting kind and acknowledging what is not advisible with PHP as an Apache SAPI.
 [2021-10-18 13:33 UTC] cmb@php.net
-Status: Assigned +Status: Verified -Type: Bug +Type: Documentation Problem -Assigned To: cmb +Assigned To:
 [2021-10-18 13:33 UTC] cmb@php.net
Thank you for the thorough analysis!

> Maybe the solution would be more of the documenting kind and
> acknowledging what is not advisible with PHP as an Apache SAPI.

I agree that this is the proper solution for now.

> See my repo: https://github.com/gmoniker/php-apache2handler/commits/master

Interesting!  Maybe you want to provide that or parts of it as
pull request[1] for the master branch (supposed to become PHP
8.2)?  This would leave ample time for testing and possible
amendments, and some minor BC breaks could be acceptable.

[1] <https://github.com/php/php-src/pulls>
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Thu Oct 10 14:01:27 2024 UTC