How it works
How a synchronous upload gate works
The only moment you can stop a webshell rather than find it is the moment it arrives. On a LiteSpeed host, that moment happens inside PHP — which is both the opportunity and the catch.
The window you are trying to close
An attacker uploads a file through a vulnerable plugin at 09:15:02 and requests it at 09:15:04. Two seconds. Any detection mechanism with a latency longer than that has, in this case, documented an incident rather than prevented one.
Asynchronous detection is genuinely valuable — most compromises are not exploited within seconds, and finding the shell in five seconds instead of eighteen hours changes everything about the cleanup. But if you want to stop the upload, the check has to happen while the request is still in flight.
Three places to put the check, and why two of them do not work here
In the application. A PHP security plugin can inspect an upload before the application stores it, and for a single site that works well. On a hosting server it does not: it has to be installed and kept current in every site, customers disable it, and it shares the fate of the process it runs in — an attacker who can already execute PHP in that process can switch it off. That is an architectural observation, not a criticism of any particular plugin.
In the web server, with ModSecurity. The @inspectFile operator hands an uploaded temporary
file to an external program that returns a verdict, and on Apache this is a well-worn path. On
OpenLiteSpeed it exists, but with two documented caveats: it applies only to dynamic requests,
and the output semantics differ from Apache’s — the script must print 1 as its first character
to block. Building a product promise on a mechanism with that much variation between server
editions is asking for a support burden. Shelltrap treats a ModSecurity adapter as a
LiteSpeed-Enterprise option after a per-build integration test, not as the general answer.
In PHP itself, before the application runs. PHP has a configuration directive,
auto_prepend_file, that loads a file before every script. Set it in the global lsphp
configuration and you have a hook that runs before any customer code, on every PHP request, on
OpenLiteSpeed and LiteSpeed Enterprise alike, with no ModSecurity involved.
That is the mechanism Shelltrap uses, and it is worth being clear that this is not a novel idea: it is the same hook other host-level PHP security products use, for the same reasons.
What actually happens
The prepended file is deliberately thin. Its first act is to check whether $_FILES is
non-empty; on the overwhelming majority of requests it is not, and the adapter does nothing
measurable at all. That matters: this code runs on every PHP request on the server.
When there is an upload, the adapter opens a local Unix socket and sends one line of JSON
describing the document root, the server name and the temporary files. The broker opens each
temporary file with O_RDONLY|O_NOFOLLOW|O_NONBLOCK|O_CLOEXEC, requires a regular file with a
matching owner, enforces the configured maximum size, and hands descriptors to a scanning worker.
A request carries at most 128 files and at most 1 MiB per protocol line.
The verdict comes back as one line: allow or deny, with a reason and a duration. malicious
means deny immediately. suspicious only means deny when the domain policy says
heuristics.action=quarantine — otherwise a heuristic score does not block an upload.
unscanned and degraded stay visible in the record but never become a deny on their own,
because a resource limit is not evidence of malice.
On deny, the adapter returns HTTP 403 and deletes the temporary file. On allow, the request continues to the application exactly as it would have.
The budget, and why it is short
The gate holds an HTTP request open while it waits, so its time budget is a user-visible
latency. upload.timeout_ms defaults to 2000 ms and is capped at 60000 ms.
Two seconds sounds arbitrary until you consider what it is trading. Longer means deeper archive
inspection and fewer files falling out as unscanned, at the cost of a visibly slower upload
form and more concurrent workers holding descriptors. Shorter means a snappier site and more
work pushed onto the asynchronous watcher behind the gate. The policy is per domain, so a photo
gallery and a document portal do not have to share an answer.
The timeout is enforced by the broker even if a scanner implementation ignores the context it was handed, and the number of workers holding descriptors is bounded. A gate that can be made to pile up is a denial-of-service vector against the site it is protecting.
Fail-open, and the argument about it
The interesting question is not what the gate does when it finds something. It is what the gate does when it cannot answer — the broker is restarting, the socket is missing, the scan timed out.
- fail-open: the upload proceeds. The site keeps working; a file may reach the disk unchecked — where the real-time watcher catches it moments later.
- fail-closed: the upload is refused with HTTP 503. Nothing unchecked gets in; the customer’s contact form or media library is broken until the scanner is back.
Shelltrap defaults to fail-open and makes fail-closed available per domain. That default is a considered position, and it is fair to disagree with it.
The reasoning: on a shared hosting server, a scanner outage that takes every customer’s upload form down with it is an outage caused by the security product. It generates pressure to remove the security product, which is a worse security outcome than the file that got through — because the file that got through is caught by the watcher seconds later anyway. The gate is a latency improvement over the watcher, not the only line of defence, and a defence-in-depth component should not be a single point of failure for the thing it defends.
For a domain where an unchecked upload really is worse than a broken form — a document portal in
a regulated environment, say — upload.on_error = closed is one policy key away, and the gate
error remains fully diagnosable either way.
The limitation we print on the box
auto_prepend_file can be overridden by a .user.ini file inside a site. An attacker who can
already write into a document root can therefore point it elsewhere and take the gate out of
their own path.
This is a real limitation and we would rather state it than have it discovered. Three things follow from it:
- That exact pattern is a heuristic hit. A
.user.inisettingauto_prepend_filefrom inside a site is one of the strongest configuration signals there is, and it is scored as one. You can see it in the Signal Explorer . - The watcher stays behind the gate as the catch-all. Removing the gate does not remove detection; it removes the two-second head start.
- Writing that
.user.iniis itself an event. It is a file write in a watched document root, and the event mask includes attribute changes precisely so that configuration-only attacks are visible.
A defence that can be disabled by an attacker who already has write access is not useless. It is one layer, and it should be described as one.
What the gate does not cover
The gate covers PHP web uploads. Files also arrive by FTP, SFTP, WebDAV, the panel file manager,
git, wget and a shell.
FTP gets its own path: pure-uploadscript calls an enqueuer that returns immediately, so the
scan happens right after the upload rather than during it. That post-upload character is stated
in the product rather than glossed over, because “we scan FTP uploads” and “we scan FTP uploads
before they land” are very different claims.
Everything else is caught asynchronously by the real-time watcher , typically within seconds.