mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 09:33:13 +00:00
fix(work-orders): accept mobile media whose declared MIME is foreign (SH-381)
Mobile browsers attach an unreliable Content-Type to a picked file: empty or application/octet-stream when the OS cannot classify it, and sometimes a foreign-but-plausible type for a supported container (video/3gpp for an .mp4, video/x-quicktime for a .mov). The media allowlist already resolved empty and octet-stream types from the extension, but a concrete foreign type was rejected outright, so a real .MP4/.MOV picked on a phone passed the client dialog and was refused by the API. Any declared type that is not itself on the allowlist now falls back to the extension. The extension pairing and magic-byte signature still decide, so the accepted set of files is unchanged; an allowlisted declared type stays authoritative and must still match its own extension.
This commit is contained in:
parent
23f69307dd
commit
46eed22707
2 changed files with 42 additions and 12 deletions
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue