Skip to content
LaravelBlog

livewire/livewire: Content-based MIME detection for temporary uploads arrives in 4.x | телеграфови (PR #10581) end --- Sortby | : Только выходной JSON. END. Перевод на русский не требуется, выходной JSON остаётся как есть, т.к. контент технический. Но сле

LaravelBlogBot
LaravelBlogBot

Contributed by calebporzio

Part of the livewire v4.4.7 release

PR #10581 ports the content-based MIME detection fix for temporary file uploads from the 3.x series (source commit 11ee40d2, PR #10440) into Livewire 4.x. From this version on, TemporaryUploadedFile::getMimeType() determines a file's type by reading the first 64 KB of its actual contents instead of trusting the MIME metadata reported by the storage disk — metadata that, on remote disks like S3, is usually derived from the file extension or supplied by the client at upload time. Files that cannot be read, or that are empty, now resolve to application/octet-stream. The change ships with 62 passing SupportFileUploads unit tests (152 assertions) and a clean composer audit.

What changed

Previously, getMimeType() asked the storage layer for the MIME type and only sniffed contents as a partial fallback:

// Old behavior (v3.8.10)
$mimeType = $this->storage->mimeType($this->path);

if (in_array($mimeType, ['application/octet-stream', 'inode/x-empty', 'application/x-empty'])) {
    $detector = new FinfoMimeTypeDetector();
    $mimeType = $detector->detectMimeTypeFromPath($this->path) ?: 'text/plain';
}

That fallback was itself extension-based (detectMimeTypeFromPath), so a file named photo.webp containing PNG bytes would still be reported as image/webp. The new implementation ignores storage metadata entirely:

// New behavior
$stream = $this->storage->readStream($this->path);

if (! is_resource($stream)) {
    return 'application/octet-stream';
}

try {
    // Avoid downloading the entire object when the temporary disk is remote...
    $contents = stream_get_contents($stream, 64 * 1024);
} finally {
    fclose($stream);
}

return (new FinfoMimeTypeDetector())->detectMimeTypeFromBuffer($contents)
    ?: 'application/octet-stream';

The result is memoized in a new $detectedMimeType property, so repeated calls on the same file instance trigger at most one streamed read. The 4.x-specific handling of test upload metadata is preserved, so faked files in your test suite continue to behave as before.

Why it matters

Validation rules like mimes: and mimetypes: are evaluated against the MIME type reported by the uploaded file. When that type came from extension-derived disk metadata, a mismatch between a file's name and its contents slipped through: an attacker could upload a script or polyglot payload with an innocuous image extension and pass MIME validation, because nothing ever inspected the bytes. Content sniffing closes that vector and also fixes genuine accidents, such as users renaming files before uploading.

It also matters for remote disks specifically. Flysystem adapters for services like S3 typically resolve mimeType() from stored object metadata or the extension detector rather than the payload, so the old code path was effectively trusting the upload request itself.

What this looks like in practice

class UploadReport extends Component
{
    use WithFileUploads;

    public $report;

    public function save()
    {
        // Now evaluated against the file's real bytes, not its extension
        $this->validate([
            'report' => 'file|mimes:pdf|mimetypes:application/pdf|max:10240',
        ]);

        $this->report->store('reports');
    }
}

The new test case demonstrates the shift directly: a PNG is stored on an S3-like disk as png-disguised-as.webp. The disk reports image/webp, but the uploaded file now returns image/png, passes mimes:png|mimetypes:image/png, and fails mimes:webp|mimetypes:image/webp — with only a single readStream call recorded thanks to memoization.

New fallback and edge-case behavior

The old fallback chain (extension guessing, then text/plain) is gone. Three cases now resolve deterministically:

  • Unreadable files (stream cannot be opened) → application/octet-stream
  • Empty files (zero bytes read) → application/octet-stream, never the storage metadata
  • Unidentifiable content (finfo cannot classify the buffer) → application/octet-stream

Plain-text content is still detected as text/plain by finfo in the vast majority of cases, but binary blobs with unrecognizable formats will no longer be rescued by their file extension.

Upgrade impact from v3.8.10

There are no public API changes — getMimeType() keeps its signature — but return values can differ in these situations:

  • Files whose contents don't match their extension now report the content-derived type. Code or tests asserting the extension-based value will need updating.
  • Empty, unreadable, or exotic files report application/octet-stream instead of an extension guess or text/plain.
  • On remote temporary-upload disks, requesting a MIME type now performs one small streamed read (at most 64 KB) instead of a metadata lookup — a negligible cost for a significant accuracy gain.

Since this is the same fix already shipped in the 3.x series, behavior between your 3.x and 4.x deployments is now aligned, which simplifies any staged migration.

Takeaways

  • Temporary upload MIME detection is now content-based: the first 64 KB of bytes is inspected with finfo, and storage/extension metadata is never trusted.
  • mimes: and mimetypes: validation on Livewire uploads is stricter and spoof-resistant; mismatched files will now be correctly rejected.
  • Empty, unreadable, or undetectable files consistently return application/octet-stream.
  • MIME results are cached per file instance, so remote disks see at most one partial read per file.
  • No public API changes — most applications need no code changes, but review tests that relied on extension-derived MIME values.

Sources

More from this release

Related Articles