File uploads are protected by GlobalPostFileUploadGuard, a
request filter that validates uploaded files before controller code runs. Its
job is to reject dangerous uploads early using extension checks, MIME checks,
magic-byte inspection, filename validation, and size limits.
app/Filters/GlobalPostFileUploadGuard.php.
The filter is aliased in app/Config/Filters.php and is also
applied globally in the before filter chain.
The filter returns immediately unless all of the following are true:
POST
Non-POST requests are ignored by the guard.
If $request->getFiles() is empty, the filter exits without doing anything.
Oversized uploads that fail at the PHP upload layer are still blocked and logged with a specific reason.
The filter also recurses through nested file input arrays, so it protects both single-file and multi-file form structures.
The allowlist is defined through $allowedMimeMap. The filter
resolves an expected MIME from the client extension and rejects any extension
that does not map to an approved MIME.
| MIME type | Allowed extensions |
|---|---|
image/jpeg | jpg, jpeg |
image/png | png |
image/gif | gif |
image/webp | webp |
application/pdf | pdf |
application/msword | doc |
application/vnd.openxmlformats-officedocument.wordprocessingml.document | docx |
application/vnd.oasis.opendocument.text | odt |
text/rtf / application/rtf | rtf |
application/vnd.ms-excel | xls |
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | xlsx |
application/vnd.oasis.opendocument.spreadsheet | ods |
text/csv / application/csv | csv |
text/plain | txt |
zip,
rar, and 7z are blocked. The filter also separates
xlsx from old Excel MIME handling and keeps txt and
csv distinct.
The guard maintains a large denylist in $blockedExtensions to
stop common executable, script, archive, config, and sensitive file types.
| Category | Examples |
|---|---|
| PHP / server code | php, phtml, phar, jsp, asp, aspx |
| Scripts | js, ts, jsx, tsx, sh, bash, ps1, bat, cmd |
| Binaries | exe, dll, msi, apk, deb, rpm, bin |
| Archives | zip, rar, 7z, tar, gz, iso |
| Config / secrets | env, ini, htaccess, htpasswd, key, pem, p12 |
| Database / logs | sql, db, sqlite, log, bak |
| Markup / risky text | html, htm, xhtml, xml, svg |
Multiple extensions are handled defensively. If a filename like
invoice.php.pdf or report.jpg.js contains any blocked
extension in its middle segments, the file is rejected.
Each uploaded file passes through this validation order:
If PHP reports an invalid upload and the error is a server/form size issue, the guard blocks immediately.
Rejects null bytes, path separators, and filenames longer than 255 characters.
Rejects files that hide blocked extensions inside multi-part names.
Rejects uploads whose client extension is directly on the blocked list.
The hard application limit is 25 MB.
Uses PHP finfo(FILEINFO_MIME_TYPE) on the temporary uploaded file.
Maps the client extension to one expected MIME from the allowlist.
Checks the actual file header against known signatures for supported formats.
The detected MIME must match the expected MIME exactly; generic fallback MIME values are not accepted.
if ($request->getMethod() !== 'post') {
return;
}
$files = $request->getFiles();
if (empty($files)) {
return;
}
The guard performs deep header checks using $magicBytes for
several formats:
| Type | Signature rule |
|---|---|
| JPEG | FF D8 FF |
| PNG | 89 50 4E 47 0D 0A 1A 0A |
| GIF | GIF87a or GIF89a |
%PDF- | |
| DOC / XLS (legacy) | D0 CF 11 E0 |
| DOCX / XLSX / ODT / ODS | PK 03 04 |
| WebP | Special-case check for RIFF....WEBP |
There is also an scanForEmbeddedCode() method in the filter, but
its invocation is currently commented out. The active protection path today is
the filename, extension, MIME, and magic-byte validation sequence.
This guard is registered in two relevant places:
| Location | Effect |
|---|---|
app/Config/Filters.php global before filters |
Applies the guard to incoming requests globally before controller execution. |
app/Config/Routes.php employeeRest group |
Also explicitly includes GlobalPostFileUploadGuard alongside rate-limit, app-signature, and JWT auth filters. |
$routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratelimit', 'appSignature', 'authJWT']], function ($routes) {
// upload-related endpoints live here
});
When the filter rejects a file, it logs a critical event and immediately sends
a JSON error response with HTTP 403.
| Response field | Meaning |
|---|---|
status | error |
message | Security-policy rejection message including the reason. |
debug | Detailed rejection reason only when ENVIRONMENT === 'development'. |
{
"status": "error",
"message": "File upload rejected: Security policy violation. Reason: MIME-extension mismatch.",
"debug": "MIME-extension mismatch"
}
The log entry includes the block reason, client IP, URI, input field, original filename, MIME, extension, and size.
The filter sends the response directly and exits, so later controller logic does not run for rejected uploads.
The extension is only used to resolve the expected MIME; the real file MIME and header still have to match.
Server-side PHP upload limits can still reject larger files earlier, and the filter explicitly handles that error path.
Because the check is strict, any environment mismatch in MIME detection can cause a block until the allowlist is updated deliberately.