Skip to content

WireUpload rejects valid MP3 uploads ("File content does not match its extension"); no config way to add allowed MIME types #2287

Description

@outflux3

WireUpload rejects valid MP3 files: "File content does not match its extension" — and $config->fileContentTypes array values (per docblock) aren't honored

Short description

WireUpload::hasValidMimeType() rejects some valid .mp3 uploads with "File content does not match its extension", because finfo reports a MIME type other than audio/mpeg for certain MP3s (e.g. files with large ID3v2 tags / embedded cover art, or produced by particular encoders).

This affects normal InputfieldFile/InputfieldImage uploads (not just custom WireUpload usage), so editors can't upload legitimate audio.

The natural workaround — supplying additional allowed MIME types via $config->fileContentTypesdoes not work, even though hasValidMimeType()'s own docblock says array values are supported. The only config-level remedies are to remove the extension from fileContentTypes entirely (disabling the check) or replace its single MIME string.

Steps to reproduce

  1. Have a file field (e.g. a FieldtypeFile) that allows the mp3 extension.
  2. Upload a valid MP3 whose content finfo does not detect as audio/mpeg (common with classical/audiobook rips carrying large ID3v2 tags / embedded artwork).
  3. The upload is rejected in the field UI with: <filename> - File content does not match its extension.

Other MP3s in the same batch (detected as audio/mpeg) upload fine, so it's file-content dependent.

Confirm the detected type:

$finfo = finfo_open(FILEINFO_MIME_TYPE);
echo finfo_file($finfo, '/path/to/rejected.mp3'); // e.g. application/octet-stream (not audio/mpeg)

Expected

A valid MP3 with the .mp3 extension should upload to a field that permits mp3. Failing that, there should be a supported, documented config way to add accepted MIME types for an extension.

Actual

  • The file is rejected.
  • $config->fileContentTypes['mp3'] = ['audio/mpeg', 'audio/mp3', 'application/octet-stream'] (array) is not honored — see root cause below.

Concrete reproduction data

A real-world failing file: a valid MP3 (plays fine everywhere) beginning with an ID3 v2.3 header and carrying a ~1.4 MB ID3v2 tag (embedded cover art) before the first MPEG audio frame.

finfo results for the same file across PHP builds on one machine:

PHP build (finfo/libmagic) Detected MIME Result
PHP 8.3.30 (Apache/FastCGI — the one actually serving the site) application/octet-stream rejected
PHP 8.4 / 8.5 (CLI) audio/mpeg would pass
system PHP (CLI) audio/mpeg would pass
file --mime-type (system libmagic) application/octet-stream

So the identical, valid MP3 is accepted or rejected purely based on the PHP/libmagic version. The large leading ID3v2 tag appears to push the MPEG frame sync beyond some libmagic builds' detection window, so they fall back to application/octet-stream. Because the accepted MIME list for mp3 is hardcoded, there is no supported way to accommodate this.

Root cause

In wire/core/Tools/WireUpload/WireUpload.php:

  • saveUpload() calls hasValidMimeType($destination) and errors on failure:

    if(!$this->hasValidMimeType($destination)) {
        $fname = $this->wire()->sanitizer->name(basename($destination));
        $this->error("$fname - " . $this->_('File content does not match its extension'));
        ...
    }
  • hasValidMimeType()'s docblock advertises array values per extension:

    /**
     * @param array $mimeTypes Optionally specify [ 'ext' => 'type', 'ext' => [ 'type1', 'type2' ] ...
     */
    public function hasValidMimeType($filename, $mimeTypes = []) {
        if(empty($mimeTypes)) $mimeTypes = $this->wire()->config->fileContentTypes;
        ...
        // trim off the force download "+" used by $config->fileContentTypes
        foreach($mimeTypes as $key => $mimeType) {
            $mimeTypes[$key] = ltrim($mimeType, '+');   // ← ltrim() on an array = TypeError (PHP 8)
        }
        ...
        $allowedType = $mimeTypes[$extension];
        if(stripos($mimeType, $allowedType) === 0) return true;   // ← stripos() needle assumed string
        ...
    }

    So when a $config->fileContentTypes value is an array, ltrim() / stripos() throw (needle must be a string). In practice values must be single strings, contradicting the docblock.

  • The set of MIME types that can match multiple values is a hardcoded map, not extendable via config:

    $multiMimeTypes = [
        'svg'  => [ 'image/svg+xml', 'application/xml', 'text/xml' ],
        'docx' => [ 'application/vnd.openxmlformats-officedocument', 'application/zip' ],
        'xlsx' => [ 'application/vnd.openxmlformats-officedocument', 'application/zip' ],
        'zip'  => [ 'application/zip', 'application/x-zip' ],
        'mp3'  => [ 'audio/mpeg', 'audio/mp3' ],
    ];

    Because mp3 maps only to audio/mpeg / audio/mp3, any other finfo result (e.g. application/octet-stream) is rejected, and there's no supported way to add to this list.

Current workarounds (site config.php)

// Disable the content check for mp3 (extension allowlist still applies):
$fct = $config->fileContentTypes;
unset($fct['mp3']);
$config->fileContentTypes = $fct;

// …or replace the single value (loses audio/mpeg unless left to the hardcoded multiMimeTypes):
// $config->fileContentTypes = array_merge($config->fileContentTypes, ['mp3' => 'application/octet-stream']);

Both are blunt: the first removes the safeguard entirely; the second can't express "these several MIME types are OK."

Suggested fix

  1. Honor array values in $config->fileContentTypes / hasValidMimeType($mimeTypes) as the docblock already promises — normalize the ltrim('+') step and the matching loop to handle a string or an array of allowed types per extension. This lets sites self-serve:

    $config->fileContentTypes = array_merge($config->fileContentTypes, [
        'mp3' => ['audio/mpeg', 'audio/mp3', 'audio/x-mpeg', 'application/octet-stream'],
    ]);
  2. And/or broaden the hardcoded mp3 list to include commonly-reported valid variants (finfo/libmagic differs across platforms).

Fix #1 is the more general win — it aligns the code with the documented behavior and covers every extension, not just mp3.

Environment

  • ProcessWire: 3.0.268 (logic is present in 3.0.x generally)
  • PHP: 8.3.30 on the serving stack (Apache + FastCGI); reproduced where finfo returns application/octet-stream for a valid MP3. Other local PHP builds (8.4 / 8.5 CLI) return audio/mpeg for the same file — i.e. the outcome is libmagic-version dependent, not ProcessWire-version dependent.
  • OS: macOS
  • Reproducible with any valid MP3 that carries a large leading ID3v2 tag (embedded artwork) on a PHP/libmagic build that reports it as application/octet-stream.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions