Some request body control techniques by example of php2shell¶
2026-10-21
Today, critical php updates are making the rounds, and you really should patch ASAP. Nevertheless, some users of Vinyl Cache might be interested in ways of reducing the attack surface.
This short article is based on a mastodon thread.
Important
The advice given in this short article does not, by any means, imply to provide full protection from exploitation of any security issues. It might help reduce the likelihood of successful exploits or be totally irrelevant. It is given with the intention of being helpful and to educate about Vinyl Cache and VCL.
Background¶
As per CVE-2026-109618, a relevant vector to exploit this and other bugs is parsing of multipart request bodies, so we look into ways of avoiding these to be sent to a vulnerable backend.
Rejecting multipart/form-data requests¶
As per RFC7578, multipart/form-data request bodies are indicated by a
Content-Type header field with that prefix.
Before considering to reject such requests, we can inspect live traffic using
vinyllog:
vinyllog -dCq 'ReqHeader:Content-Type ~ multipart/form'
vinyllog -Cq 'ReqHeader:Content-Type ~ multipart/form'
The first command scans the in-memory log, the second runs until interrupted.
If we see no such requests (after waiting for some time, ideally testing important functions of the site(s) behind Vinyl Cache), we can consider to just block them in VCL:
sub recv_no_multipart {
if (! req.http.Content-Length && ! req.http.Transfer-Encoding) {
return;
}
if (req.http.Content-Type ~ "(?i)^multipart/form-data") {
return (synth(415));
}
}
sub vcl_recv {
call recv_no_multipart;
# ...
}
The first if block in sub recv_no_multipart returns from the subroutine
if there is no request body, as indicated by neither the Content-Length nor
the Transfer-Encoding header field being present.
The second if block checks if the content-type is multipart/form-data
(case insensitive) and, if so, triggers a 415 Unsupported Media Type
response.
As always in VCL, the recv_no_multipart subroutine can also be called
selectively under specific circumstances only.
Note
Some resources on similar mitigation techniques suggest to match on request
methods like POST or PUT. While this is not exactly wrong as long as
all accepted methods are covered, a client can send a request body with any
request method, so this code just checks the relevant header fields.
Limiting request body sizes and avoiding chunked transfer encoding¶
Another option (which can be used in addition to the previous technique, also) is to limit the size of request bodies, which should reduce the likelihood of an Out Of Memory (OOM) condition to be hit. Here’s a VCL example to do this:
import std;
sub vcl_recv {
if (! std.cache_req_body(1M) &&
(req.http.Content-Length || req.http.Transfer-Encoding)) {
return(synth(413));
}
# ...
}
std.cache_req_body(1M) instructs Vinyl Cache to read up to 1MB of request
body data into RAM (actually into the Transient storage, which can be
redefined, but defaults to malloc). The function returns true if caching
succeeded, in which case we continue with vcl_recv processing. If caching did
not succeed, it could also be because there was no request body, so next we
check if any of the header fields indicating one is present and only then
trigger a 413 Content Too Large response. This way, requests without a body,
such as with the GET method, do not trigger an error.
The primary use case of std.cache_req_body() is to enable retries of backend requests with request bodies, but here we make use of the fact that it fails if the request body is larger than the specified limit. If this limit is significantly below PHP’s memory limit, we make it at least much harder to trigger OOM conditions.
Important
You will likely need to adjust the allowed request body size (1M in the
example). Make it as small as possible, but larger than what the application
requires. Consider using different values for different paths, if necessary.