Skip to content

[2.x] fix: stop rejecting uploads over two spellings of the same MIME type - #515

Open
ekumanov wants to merge 1 commit into
FriendsOfFlarum:2.xfrom
ekumanov:fix/mime-cross-validation-synonyms
Open

ekumanov wants to merge 1 commit into
FriendsOfFlarum:2.xfrom
ekumanov:fix/mime-cross-validation-synonyms

Conversation

@ekumanov

@ekumanov ekumanov commented Sep 2, 2026

Copy link
Copy Markdown

Fixes #514.

The problem

getMimeType() cross-validates php-mime-detector against fileinfo and rejects the upload whenever the two answers are not the same string. Most formats have more than one registered name, and the two libraries did not always pick the same one, so a file that is exactly what it claims to be is deleted and the member is shown MIME type mismatch detected: audio/vnd.wave vs audio/x-wav.

.wav is the case that surfaced it — audio/vnd.wave is the IANA registration, audio/x-wav is the name that predates it, and no .wav exists for which the two libraries answer differently, so the format is unavailable on every install. Measuring the rest of the surface (real files wherever a header is not enough; samples fileinfo could not identify at all discarded), 13 of 34 identifiable formats are rejected unconditionally on PHP 8.4 / libmagic 5.45: wav, flac, aiff, m4a, opus, avi, rar v4 and v5, rtf, xml, ttf, ico, deb. Issue #514 has the full table.

The fix

Compare the formats, not the strings.

  • EQUIVALENT_MIME_TYPES maps alternative spellings onto one representative each; canonicalMimeType() applies it, after lower-casing and dropping any parameters, so text/XML; charset=utf-8 no longer differs from text/xml. A name that is not in the table is returned unchanged, so anything nobody has listed still has to match exactly.
  • The comparison is the only thing that changes. getMimeType() still returns $detectorMime — the detector's own spelling — so the admin's MIME whitelist and the extension mapping see exactly what they saw before.
  • The table is a protected const read through static::, so an install with an unusual libmagic build can extend it in a subclass.

Two groupings are wider than pure synonyms, and are commented where they appear:

  • Ogg. libmagic names the container (audio/ogg), php-mime-detector the codec inside it (audio/opus). Same bytes, and the container is what an admin whitelisting audio/ogg means.
  • SFNT fonts. TrueType and OpenType share the container, and some libmagic builds report font/sfnt for either without distinguishing them. TTF against OTF is not a security boundary.

Why this doesn't weaken the check

Nothing in the table crosses a format boundary, so a file whose magic bytes and whose libmagic signature describe genuinely different formats is still rejected.

I ran every fixture in tests/fixtures through both the current class and the patched one, on Ubuntu 24.04 / libmagic 5.45:

fixture 2.x today with the fix
Polyglot.flif REJECTED REJECTED
Polyglot.jpg text/html text/html
SpoofedMime.png text/html text/html
TextFileWithPngExtension.png text/plain text/plain
Malicious.html text/html text/html
Malicious.svg / Safe.svg image/svg+xml image/svg+xml
Example.apk application/vnd.android.package-archive same
Document.pdf / Example.zip / *.jpg / Plain.txt unchanged unchanged

Identical in every row. Polyglot.flif is the one the mismatch branch actually catches, and it still does: image/flif against text/html is a real disagreement. The other malicious fixtures are caught downstream by the whitelist, as they are today.

And the formats that change, same harness:

2.x today with the fix
wav REJECTED audio/vnd.wave
flac REJECTED audio/x-flac
aiff REJECTED audio/aiff
m4a REJECTED audio/mp4
opus REJECTED audio/opus
avi REJECTED video/vnd.avi
rar v4 / v5 REJECTED application/x-rar-compressed
rtf REJECTED application/rtf
xml REJECTED application/xml
ttf REJECTED font/ttf
ico REJECTED image/x-icon
deb REJECTED application/x-deb

13 formats go from rejected to accepted; none go the other way, and every returned value is the detector's own spelling, unchanged from what the whitelist matched against before.

Tests

Added to tests/unit/Mime/MimeTypeDetectorTest.php:

  • a real WAV round-trip, asserting both that it is accepted and that audio/vnd.wave is still what callers get — this one fails on 2.x today;
  • a genuine mismatch (FLIF header, HTML body — the shape of the Polyglot.flif fixture) that must still be rejected;
  • the equivalence table in both directions, as data providers: 14 pairs of spellings that must merge, 5 pairs of formats that must stay apart, plus pass-through of an unlisted type and of null.

composer test:unit 167/167 green, PHPStan level 6 clean. The two libmagic-dependent tests skip themselves where fileinfo is missing or does not identify a WAV.

No frontend change, so no js/dist.

No .wav can be uploaded to a forum running this extension. The user is shown
`MIME type mismatch detected: audio/vnd.wave vs audio/x-wav` and the file is
deleted. Both names mean a RIFF/WAVE file: php-mime-detector returns the IANA
registration, libmagic returns the name that predates it, and there is no .wav
either library would answer differently for.

The cross-validation in `getMimeType()` compares the two answers with `!==`, so
any format the two libraries have picked different registered names for is
rejected outright. Surveying every format php-mime-detector can identify, with
real files and discarding samples fileinfo could not identify at all, 13 of 34
are unconditionally rejected on PHP 8.4 / libmagic 5.45: wav, flac, aiff, m4a,
opus, avi, rar (both generations), rtf, xml, ttf, ico and deb.

- Compare formats rather than strings, resolving alternative spellings through
  `EQUIVALENT_MIME_TYPES` first. `getMimeType()` still returns the detector's
  own spelling, so the MIME whitelist and the extension mapping see no change.
- Nothing in the table crosses a format boundary, so the check keeps its
  purpose. Two groupings are wider than pure synonyms and are commented where
  they appear: Ogg, where libmagic names the container and php-mime-detector
  the codec inside it, and the SFNT font family, where some libmagic builds
  report `font/sfnt` for both TrueType and OpenType.
- Canonicalisation also lower-cases and drops parameters, so
  `text/XML; charset=utf-8` no longer differs from `text/xml`.

Every fixture in `FileUploadSecurityTest` behaves exactly as before, Polyglot
.flif included — a FLIF header on an HTML body is still a mismatch, because
`image/flif` and `text/html` are different formats.

Tests: a real WAV round-trip (fails on 2.x today), a genuine mismatch that must
still be rejected, and the equivalence table both ways — spellings that must
merge, and formats that must stay apart.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ekumanov
ekumanov requested a review from a team as a code owner September 2, 2026 23:23
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.

Uploads are rejected when php-mime-detector and fileinfo use different names for the same format (no .wav can be uploaded)

1 participant