Skip to content

Derive uploaded file extension from the validated MIME type - #320

Merged
roncodes merged 1 commit into
fleetbase:release/v1.6.74from
shivanathd:mindcat/upload-extension-from-mime
Oct 23, 2026
Merged

roncodes merged 1 commit into
fleetbase:release/v1.6.74from
shivanathd:mindcat/upload-extension-from-mime

Conversation

@shivanathd

Copy link
Copy Markdown

FileController::upload (internal and v1) names stored files through File::randomFileNameFromRequest(), which keeps getClientOriginalExtension(). UploadFileRequest checks only the type detected from the file's contents, and its allowed list includes application/octet-stream, text/plain, text/javascript and the image types. So a file whose contents pass that check can still be stored as .php, .phtml and so on, for example a valid PNG with extra bytes appended and a .php name.

The default Docker image does not link public/storage, so it is not exposed. On installs that have run php artisan storage:link, or that serve the upload disk from a PHP-enabled web root, a file stored with a script extension could be executed by the web server. This PR is hardening so the stored name never depends on that deployment detail.

Change

New File::storageExtensionForUpload(UploadedFile $file):

  1. Keep the client's extension (lower-cased) when it is one of the extensions for the detected MIME type (Symfony MimeTypes), or when it is a plain data format that detection often reports loosely: csv, tsv, txt, json, xls(x), doc(x), ppt(x), pdf, archives, certificate formats. This keeps imports working, e.g. a CSV detected as text/plain or an XLSX detected as application/zip.
  2. Otherwise use guessExtension() (from the detected type), falling back to bin.
  3. Never return php, php3–php8, phtml, phar, pht, phps, shtml, htaccess or cgi; those become bin.

It is used by randomFileNameFromRequest() (when no explicit extension is passed) and by FileResolverService::resolveFromUpload(), which had the same pattern. Ordinary uploads keep their names: photo.png stays .png, orders.csv stays .csv. A PNG uploaded as photo.jpg is now stored as .png. original_filename is unchanged.

Not changed here: SVG uploads are still stored as .svg because that is their real type. The disk and path inputs are still taken from the request; restricting disk to configured disks could be a follow-up if you want it.

Tests

  • FileModelTest: new case covering:
    • photo.PNG → png;
    • PNG named .jpg → png;
    • PNG + PHP named shell.php → png;
    • shell.phtml → png;
    • no extension → png;
    • orders.csv → csv;
    • config.json → json;
    • text named .phar → txt;
    • unknown binary named .php5 → bin;
    • an explicit extension passed to randomFileNameFromRequest() still wins.
  • FileControllerTest: an upload named shell.php with PNG contents is stored under .png. The file_controller_upload_request() helper gains an optional original-name argument (the default is unchanged).

I ran these on PHP 8.2 with bcmath and gd:

  • FileControllerTest: 19/19 pass.
  • FileResolverServiceTest: 7/7 pass.
  • FileModelTest: 12/13 pass. The failure is it exposes file uploader relationship metadata (no DB connection when the file runs on its own), which fails the same way on main.

Both new cases fail on main. php-cs-fixer --dry-run is clean on the four changed files. phpstan on File.php and FileResolverService.php gives the same count as main. The one existing finding about $request->file() possibly being an array now points at the new call instead of getClientOriginalExtension().

This PR was prepared with an AI coding tool (Claude Code) and reviewed by Shiv. Found by Mindcat (mindcat.ai).


Generated by Claude Code

Uploads were stored under the client's original extension, while
UploadFileRequest validates only the type detected from the contents. A
file whose contents pass that check could therefore be stored as .php or
.phtml, for example a PNG with PHP appended.

Add File::storageExtensionForUpload(). It keeps the client's extension when
it matches the detected type or is a plain data format that detection
reports loosely (CSV, XLSX, JSON, certificates and so on), otherwise takes
the extension of the detected type, falling back to "bin", and never
returns an extension a web server may execute. Use it for multipart uploads
(randomFileNameFromRequest) and for FileResolverService uploads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (494d885) to head (f4deb0d).

Additional details and impacted files
@@             Coverage Diff             @@
##                main      #320   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
- Complexity      8935      8941    +6     
===========================================
  Files            473       473           
  Lines          28067     28075    +8     
===========================================
+ Hits           28067     28075    +8     
Flag Coverage Δ
backend 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@roncodes roncodes mentioned this pull request Oct 23, 2026
2 of 3 tasks
@roncodes
roncodes changed the base branch from main to release/v1.6.74 October 23, 2026 05:19
@roncodes
roncodes merged commit 018c308 into fleetbase:release/v1.6.74 Oct 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants