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.

i
Where this logic lives The implementation is in app/Filters/GlobalPostFileUploadGuard.php. The filter is aliased in app/Config/Filters.php and is also applied globally in the before filter chain.

Overview

flowchart TD A[POST request with files] --> B[Run upload guard filter] B --> C[Walk uploaded inputs] C --> D[Validate one file] D --> E[Check filename and extension] E --> F[Check file size] F --> G[Detect real MIME] G --> H[Resolve expected MIME] H --> I[Check magic bytes] I --> J[Verify strict MIME match] J --> K[Allow request] E --> X[Block upload] F --> X G --> X H --> X I --> X J --> X

When it runs

The filter returns immediately unless all of the following are true:

  1. The request method is POST

    Non-POST requests are ignored by the guard.

  2. The request actually contains uploaded files

    If $request->getFiles() is empty, the filter exits without doing anything.

  3. Each uploaded file is valid enough to inspect

    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.

Allowed file types

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 typeAllowed extensions
image/jpegjpg, jpeg
image/pngpng
image/gifgif
image/webpwebp
application/pdfpdf
application/msworddoc
application/vnd.openxmlformats-officedocument.wordprocessingml.documentdocx
application/vnd.oasis.opendocument.textodt
text/rtf / application/rtfrtf
application/vnd.ms-excelxls
application/vnd.openxmlformats-officedocument.spreadsheetml.sheetxlsx
application/vnd.oasis.opendocument.spreadsheetods
text/csv / application/csvcsv
text/plaintxt
!
Notable exclusions SVG is explicitly removed. Archive formats such as zip, rar, and 7z are blocked. The filter also separates xlsx from old Excel MIME handling and keeps txt and csv distinct.

Blocked extensions

The guard maintains a large denylist in $blockedExtensions to stop common executable, script, archive, config, and sensitive file types.

CategoryExamples
PHP / server codephp, phtml, phar, jsp, asp, aspx
Scriptsjs, ts, jsx, tsx, sh, bash, ps1, bat, cmd
Binariesexe, dll, msi, apk, deb, rpm, bin
Archiveszip, rar, 7z, tar, gz, iso
Config / secretsenv, ini, htaccess, htpasswd, key, pem, p12
Database / logssql, db, sqlite, log, bak
Markup / risky texthtml, 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.

Validation flow

Each uploaded file passes through this validation order:

  1. Upload validity check

    If PHP reports an invalid upload and the error is a server/form size issue, the guard blocks immediately.

  2. Filename safety check

    Rejects null bytes, path separators, and filenames longer than 255 characters.

  3. Multiple-extension detection

    Rejects files that hide blocked extensions inside multi-part names.

  4. Forbidden extension check

    Rejects uploads whose client extension is directly on the blocked list.

  5. File size limit

    The hard application limit is 25 MB.

  6. Real MIME detection

    Uses PHP finfo(FILEINFO_MIME_TYPE) on the temporary uploaded file.

  7. Expected MIME resolution

    Maps the client extension to one expected MIME from the allowlist.

  8. Magic byte validation

    Checks the actual file header against known signatures for supported formats.

  9. Strict MIME match

    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;
}

Magic bytes check

The guard performs deep header checks using $magicBytes for several formats:

TypeSignature rule
JPEGFF D8 FF
PNG89 50 4E 47 0D 0A 1A 0A
GIFGIF87a or GIF89a
PDF%PDF-
DOC / XLS (legacy)D0 CF 11 E0
DOCX / XLSX / ODT / ODSPK 03 04
WebPSpecial-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.

Route coverage

This guard is registered in two relevant places:

LocationEffect
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
});

Blocked response

When the filter rejects a file, it logs a critical event and immediately sends a JSON error response with HTTP 403.

Response fieldMeaning
statuserror
messageSecurity-policy rejection message including the reason.
debugDetailed 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.

Operational notes

  1. Controller code never sees blocked files

    The filter sends the response directly and exits, so later controller logic does not run for rejected uploads.

  2. Client extension alone is never trusted

    The extension is only used to resolve the expected MIME; the real file MIME and header still have to match.

  3. 25 MB is the app-level limit

    Server-side PHP upload limits can still reject larger files earlier, and the filter explicitly handles that error path.

  4. False positives are possible if MIME support differs by environment

    Because the check is strict, any environment mismatch in MIME detection can cause a block until the allowlist is updated deliberately.

+
Practical takeaway This is a defensive upload gate, not just a UI validator. If a new file type must be accepted, update the allowlist, magic-byte rules, and operational expectations together rather than changing only the frontend.