> There are some limitations to this exploit, for example, the user input must be 4GB in size (which is a large amount of data and may not be possible due to the configuration of some webservers and load balancers).
I'm thinking PHP's built in request size limit config items (e.g. post_max_size) will prevent this from being an issue too. Might not stop this bug if the URL is in a header though.
As a sibling comment pointed out, PHP has max_execution_time which I think defaults to 30 seconds, but I'd guess plenty of people have set it to unlimited to work around some problem or other.
A beefy server should be able to process 4GB of data within 30 seconds though I suppose.
max_execution_time starts when the entire request has been received (or, more accurately, once PHP script execution has begun). Upload times don't trigger it to start.
If I recall correctly, max_input_time includes time taken to copy the file to the tmp directory and to parse the file for metadata, but not the upload time either (it does start prior to max_execution_time though). This value is set to -1 by default meaning it uses the value from max_execution_time instead.
You're more likely to run into Apache, Nginx, or PHP FPM timeouts. I can't remember exactly which timeout, but I remember there is one in Apache or FPM that is affected by upload time.
I don't think I would say "exceptionally well" as a rule. Compression algorithms often leave a lot on the table for very long repetitions. For example, gzip can only encode repetitions up to 258 bytes, and the absolute best case for encoding that costs 2 output bits. And then a 4GB file of the same character over and over compresses to a 4MB file primarily made of... the same character over and over.
I think that classifies as exceptionally well. You get 1000x compression and when trying to send a compressed payload, the issue of "the server may time out the connection" pretty much goes away. 4MB is trivial to send from almost anywhere - to hit the 30s timeout, you'd need to go slower than 137KB/s.
It could trivially enable millions or billions to 1 compression, though. It just needs the option of a longer length field. With even a single megabyte of repeated data, it has to split it into four thousand chunks. That's not exceptional in my book.
I'll call it exceptional when it can encode any real-drive-size repeated-character file into the size of a tweet.
IIRC (and I haven't done any php in quite a while) upload_max_size is dependent on post_max_size, so I'd think there might be a ton of php deployments out there configured for large file uploads where this exploit precondition is met after all.
While this should be fixed, I think it's always a bad idea to rely on implementation details for safety. By which I mean those filters exist for something other than protecting a system command from injection. Even if they did work properly today, there's nothing saying that this couldn't change in the future because what's considered a valid domain changes. In this case it seems like escapeshellcmd would be more appropriate.
Doesn't change the vulnerability in question, but still worth mentioning : that's not how you're supposed to sanitize commands and args you send to system() and the like.
Every time I've written code that executes external processes in PHP, I've used escapeshellarg(), even if I believe I've sanitized the input beforehand.
Sanitisation is relying on parsing (tricky as we see here) and on removing "special" characters (meaning that some otherwise valid values can't be represented)
What you should be using is quoting and escaping for the specific context.
Here in PHP and in the shell command argument position, you'd use `escapeshellarg()`. This will produce a correctly quoted and escaped string that can be safely used at argument position in a shell command.
It also doesn't rely on knowledge of the specific domain of the argument and it doesn't parse the argument at all. It's stateless and works everywhere.
Of course, if the input isn't a valid hostname, to come back to this article, `ping` will still fail, but there will be no possibility for arbitrary code execution (of course, neither their would be if `filter` worked right, but that's a) accidental (because ; and ' are not valid host name characters) and b) obviously not a given because filtering and sanitisation is much harder than dumb quoting.
Always quote. Only validate if you need to produce a readable error message. But never rely on validation or sanitisation.
Escaping shell arguments is still fighting self-erected bugstacles. Why run the sub process in a shell in the first place? Why make a string out of your argument list for the shell to parse it back to a list to finally call the process? Just exec the program directly with the argument list and do not escape anything. Just skip the dance of serializing your arguments to a string.
Unfortunately, PHP is the one scripting language that doesn't have direct exec() support! escapeshellarg() is the only tool they give you and it requires you to not shoot yourself in the foot. Even perl has multi arg system() that execs directly.
Even escapeshellarg might not be enough, because the command you are executing might trying to parse it as options. Depends on which command you are using, it could be exploitable. For example, there is a CTF challenge where you need to get RCE by passing a argument to wget escaped by escapeshellarg. (https://github.com/CykuTW/My-CTF-Challenges/blob/master/TSJ-...)
var_filter is more comparable to things like a form validation. You obviously don't use that for security neither. If you actually wanna pass stuff to system() or equivalent, you'll use escapeshellarg()
That is if you actually use raw php. Very few (good) people do. (Kinda like ruby). And symfony / laravel have functions for both these use cases. Symfonys process takes care of this for example
If you take away the call to `system` what are you left with?
It's not so much a vulnerability, but certainly a bug and I agree, it needs fixing. But it doesn't feel particularly urgent.
It would likely be classed as a vulnerability if the out of bounds access _itself_ exposed some kind of internal issue beyond accessing the user input data in an unusual manner.
It only becomes a vulnerability in userland code where it's in the hands of a naive implementation, such as the one posited here.
The other factor is that, as noted by others, there are likely a number of other layers that a sysadmin can put into place using readily-available PHP config options that would essentially prevent this from being possible and by default are pre-configured in such a way to prevent this.
Would be interesting to see if there is code in the wild that is as naive as the example given, but my gut says 'probably not' so again likelihood of this being an issue is very very low.
From that perspective I can understand core PHP devs opting not to pursue this immediately.
Not to mention that the defaults for PHP-FPM for example have maximum post body sizes and low enough memory limits that your not going to get even close to having a 4gb request.
The example vulnerable code is super contrived. That's not how you should escape comman line arguments. And even ignoring that, are you even allowed to give the system a 4gb long commandline?
I wonder what system allows a 4GB string to be passed as a command. A brief look at https://www.in-ulm.de/~mascheck/various/argmax/ makes me think none by far? It seems this limit is at most 2MB which is nowhere near 4GB. Am I misunderstanding something?
Yes, out of the box, max post size is normally 2MB.
Some systems will have that a bit higher to allow uploads of larger files, but the largest one I've ever seen is somewhere around 50mb, with 4gb you'd either have to have a super large php-fpm setup where each process could receive 4gb requests and deal with them, plus you'd have to have the config adjusted to receive these large posts.
The number of instances out there might not be exactly zero, but I'm pretty sure it's somewhere very close if not.
This appears to be a rare + rare + exceptionally rare = vulnerability type thing. While I do agree that it should be fixed, and certainly because the patch is a single easy fix that unit tests would catch any regressions.
But the author kind of made it seem like a big deal, while realistically nobody would write code this way. Taking user input, running the domain with host flag through filter var and then system call it?
Most libraries tend to implement validation themselves and not rely on filter_var.
But even if this was fixed, most people should know taking user input and running it via system is a bad idea and needs more than a simple filter_var filter.
Even if someone wrote code like that, they'd have to have configured their web server to accept 4GB of input somewhere, which gets passed to that function. That adds another "rare +" instance, as most web servers ship with way lower defaults than that.
<security hat>
Sounds like you are arguing in favor of keeping this bug. An attacker could use it to move laterally once behind whatever proxy/WAF limited the content body size.
</security hat>
Fair presumption but no, I agree it should be fixed. I just don't agree with the authors idea of how important it is to fix it, as it's highly unlikely to be exploitable in the wild.
Even without any proxies/WAF, PHP is always run (except in development environments with `php -S localhost:4124`) behind a web server, usually apache or nginx. Not sure about apache, but nginx defaults to a limit of 1MB for the request size (via `client_max_body_size` https://nginx.org/en/docs/http/ngx_http_core_module.html#cli...) and I can imagine apache have a similar default as well. Even if you allow requests to carry 1GB of data, this is still not exploitable, and if you have that large requests anyways, you usually find another way of doing the transfer than using plain HTTP requests.
Understood! The config conditions for this exploit are a little bit reassuring but from a historical perspective, there is likely to be a related exploit found with different conditions.
45 comments
[ 1275 ms ] story [ 3791 ms ] threadI'm thinking PHP's built in request size limit config items (e.g. post_max_size) will prevent this from being an issue too. Might not stop this bug if the URL is in a header though.
A 4GB request is a very large amount of data indeed.
A beefy server should be able to process 4GB of data within 30 seconds though I suppose.
If I recall correctly, max_input_time includes time taken to copy the file to the tmp directory and to parse the file for metadata, but not the upload time either (it does start prior to max_execution_time though). This value is set to -1 by default meaning it uses the value from max_execution_time instead.
You're more likely to run into Apache, Nginx, or PHP FPM timeouts. I can't remember exactly which timeout, but I remember there is one in Apache or FPM that is affected by upload time.
I'll call it exceptional when it can encode any real-drive-size repeated-character file into the size of a tweet.
https://mina86.com/2021/the-real-arg-max-part-1/
I don't believe this is what's happening at all... e and t are pointers, and they're not being written to, just reassigned.
What you should be using is quoting and escaping for the specific context.
Here in PHP and in the shell command argument position, you'd use `escapeshellarg()`. This will produce a correctly quoted and escaped string that can be safely used at argument position in a shell command.
It also doesn't rely on knowledge of the specific domain of the argument and it doesn't parse the argument at all. It's stateless and works everywhere.
Of course, if the input isn't a valid hostname, to come back to this article, `ping` will still fail, but there will be no possibility for arbitrary code execution (of course, neither their would be if `filter` worked right, but that's a) accidental (because ; and ' are not valid host name characters) and b) obviously not a given because filtering and sanitisation is much harder than dumb quoting.
Always quote. Only validate if you need to produce a readable error message. But never rely on validation or sanitisation.
That is if you actually use raw php. Very few (good) people do. (Kinda like ruby). And symfony / laravel have functions for both these use cases. Symfonys process takes care of this for example
It's not so much a vulnerability, but certainly a bug and I agree, it needs fixing. But it doesn't feel particularly urgent.
It would likely be classed as a vulnerability if the out of bounds access _itself_ exposed some kind of internal issue beyond accessing the user input data in an unusual manner.
It only becomes a vulnerability in userland code where it's in the hands of a naive implementation, such as the one posited here.
The other factor is that, as noted by others, there are likely a number of other layers that a sysadmin can put into place using readily-available PHP config options that would essentially prevent this from being possible and by default are pre-configured in such a way to prevent this.
Would be interesting to see if there is code in the wild that is as naive as the example given, but my gut says 'probably not' so again likelihood of this being an issue is very very low.
From that perspective I can understand core PHP devs opting not to pursue this immediately.
Regardless, php should fix the issue.
Some systems will have that a bit higher to allow uploads of larger files, but the largest one I've ever seen is somewhere around 50mb, with 4gb you'd either have to have a super large php-fpm setup where each process could receive 4gb requests and deal with them, plus you'd have to have the config adjusted to receive these large posts.
The number of instances out there might not be exactly zero, but I'm pretty sure it's somewhere very close if not.
But the author kind of made it seem like a big deal, while realistically nobody would write code this way. Taking user input, running the domain with host flag through filter var and then system call it?
Most libraries tend to implement validation themselves and not rely on filter_var.
But even if this was fixed, most people should know taking user input and running it via system is a bad idea and needs more than a simple filter_var filter.
Even without any proxies/WAF, PHP is always run (except in development environments with `php -S localhost:4124`) behind a web server, usually apache or nginx. Not sure about apache, but nginx defaults to a limit of 1MB for the request size (via `client_max_body_size` https://nginx.org/en/docs/http/ngx_http_core_module.html#cli...) and I can imagine apache have a similar default as well. Even if you allow requests to carry 1GB of data, this is still not exploitable, and if you have that large requests anyways, you usually find another way of doing the transfer than using plain HTTP requests.