Merge pull request #157 from Sea-Haven-Industries/fix/ab/sh-381-mobile-video-mime

fix(work-orders): accept mobile media whose declared MIME is foreign (SH-381)
This commit is contained in:
Alexandre Brandizzi 2026-09-18 22:54:48 +00:00 • committed by GitHub
commit cd4d30af4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 42 additions and 12 deletions

View file

@ -32,17 +32,18 @@ namespace SeaHaven.Services.Helpers
new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".docx" }
};
// A browser fills the multipart part's Content-Type from File.type, which mobile
// browsers leave empty (or the client sends octet-stream) when the OS cannot
// classify a picked file. Only then is the type resolved from the extension; the
// category rule, extension pairing, and magic-byte signature still decide.
private static readonly HashSet<string> UndeterminedContentTypes =
new(StringComparer.OrdinalIgnoreCase) { string.Empty, "application/octet-stream" };
// The multipart part's Content-Type comes from the browser's File.type, which is
// unreliable on mobile: it is left empty or sent as application/octet-stream when the
// OS cannot classify a picked file, and is sometimes a foreign-but-plausible video
// type the OS attaches to a supported container (e.g. video/3gpp for an .mp4,
// video/x-quicktime for a .mov). An allowlisted declared type stays authoritative and
// pairs against its own extension; anything else falls back to the extension. The
// extension pairing and magic-byte signature below still decide, so this never widens
// the accepted set of files.
private static string? ResolveContentType(string declaredType, string extension)
{
if (!UndeterminedContentTypes.Contains(declaredType))
return AllowedContentTypes.Contains(declaredType) ? declaredType : null;
if (AllowedContentTypes.Contains(declaredType))
return declaredType;
foreach (var (contentType, extensions) in ExtensionsByContentType)
{

View file

@ -2084,11 +2084,40 @@ public class WorkOrderMediaFileRulesTests
}
[Fact]
public void IsAllowed_DeclaredMimeMismatchingExtension_StillRejected()
public void IsAllowed_AllowlistedDeclaredMimeMismatchingExtension_StillRejected()
{
// A concrete declared type is authoritative; only an undetermined one falls back to the extension.
// An allowlisted declared type stays authoritative and pairs against its own
// extension, so declaring video/mp4 for a .mov (or the reverse) is still a spoof.
Assert.False(WorkOrderMediaFileRules.IsAllowed(FormFile(RealMov, "clip.mov", "video/mp4")));
Assert.False(WorkOrderMediaFileRules.IsAllowed(FormFile(RealJpeg, "photo.jpg", "image/heic")));
Assert.False(WorkOrderMediaFileRules.IsAllowed(FormFile(RealMp4, "clip.mp4", "video/quicktime")));
}
[Theory]
// SH-381: mobile browsers attach a foreign-but-plausible type to a supported container.
// The extension plus magic bytes must decide, not the unreliable declared MIME.
[InlineData("clip.mp4", "video/3gpp")]
[InlineData("VID_0001.MP4", "video/3gpp")]
[InlineData("IMG_1587.MOV", "video/x-quicktime")]
[InlineData("IMG_1587.mov", "video/mpeg")]
[InlineData("photo.jpg", "image/heic")]
public void IsAllowed_ForeignDeclaredMime_ResolvesFromExtensionAndSignature(
string fileName,
string contentType)
{
Assert.True(WorkOrderMediaFileRules.IsAllowed(
FormFile(BytesFor(fileName), fileName, contentType), WorkOrderMediaCategory.Before));
}
[Fact]
public void IsAllowed_ForeignDeclaredMime_StillRequiresMatchingSignatureAndExtension()
{
// Falling back to the extension does not weaken the gate: the bytes must still match
// the resolved type, the extension must still be supported, and a resolved document
// stays out of photo/video categories.
Assert.False(WorkOrderMediaFileRules.IsAllowed(FormFile(RealJpeg, "clip.mp4", "video/3gpp")));
Assert.False(WorkOrderMediaFileRules.IsAllowed(FormFile(RealMp4, "clip.exe", "video/3gpp")));
Assert.False(WorkOrderMediaFileRules.IsAllowed(
FormFile(RealPdf, "report.pdf", "application/x-unknown"), WorkOrderMediaCategory.Before));
}
private static byte[] BytesFor(string fileName)