How does the PHP upload gate work, and when should I use fail-closed?

The upload gate is the one place where Shelltrap prevents rather than detects. A single PHP file, loaded through auto_prepend_file in a global lsphp ini per PHP version, runs before any customer code on every PHP request.

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. When there is an upload, it 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 — not paths — to a scanning worker. A request carries at most 128 files and at most 1 MiB per protocol line.

The answer comes back as allow or deny with a reason and a duration. malicious means deny immediately: HTTP 403, and the temporary file is deleted. suspicious only denies when the domain policy says heuristics.action=quarantine. 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.

The time budget

The gate holds an HTTP request open while it waits, so its budget is user-visible latency. upload.timeout_ms defaults to 2000 ms and is capped at 60000 ms, and it is a per-domain policy: a photo gallery and a document portal do not have to share an answer.

shelltrap policy set domain 42 upload.timeout_ms=4000

Fail-open or fail-closed

The interesting question is what the gate does when it cannot answer — the broker is restarting, the socket is missing, the scan timed out.

  • fail-open (default): the upload proceeds. The site keeps working; a file may reach 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 policy set domain 42 upload.on_error=closed

Fail-open is the default because on a shared host, a scanner outage that takes every customer's upload form down with it is an outage caused by the security product — and the file that got through is caught by the watcher seconds later anyway. Choose fail-closed for a domain where an unchecked upload really is worse than a broken form: a document portal in a regulated environment, for example.

The limitation we print on the box

auto_prepend_file can be overridden by a .user.ini inside a site. An attacker who can already write into a document root can therefore take the gate out of their own path. Three things follow: that exact pattern is one of the strongest heuristic signals there is and is scored as one; the real-time watcher stays behind the gate as the catch-all; and writing that .user.ini is itself a watched file event.

What the gate does not cover

The gate covers PHP web uploads. FTP uploads are enqueued through pure-uploadscript and scanned right after the upload rather than during it. SFTP, WebDAV, the panel file manager, git, wget and the shell are caught asynchronously by the watcher, typically within seconds.

Troubleshooting

test -S /run/shelltrap/upload.sock
grep -rn 'auto_prepend_file' /usr/local/lsws/lsphp*/etc/php/*/mods-available/shelltrap.ini
cat /etc/shelltrap/upload-policy.json

A matching shelltrap.ini must exist for every lsphp ABI and point at /usr/lib/shelltrap/php/shelltrap-prepend.php.


Was this article helpful?

mood_bad Dislike 0
mood Like 0
visibility Views: 1