mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 18:53:12 +00:00
The completion-document endpoint persisted whatever file it received: the only checks were non-null, non-empty, and a 30 MB request limit. Its sibling media endpoint has enforced a MIME allowlist, MIME-to-extension pairing, and a magic-byte signature check since SH-116. Validate before the file reaches storage, so a rejected upload leaves nothing behind. An undetermined content type is accepted only alongside a .pdf name and a %PDF- signature, because the browser leaves File.type empty when the OS cannot classify the file and the completion-doc dialog already allows that.
70 lines
2.6 KiB
C#
70 lines
2.6 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
|
|
namespace SeaHaven.Services.Helpers
|
|
{
|
|
/// <summary>SH-337 file type allowlist for work-order completion documents.</summary>
|
|
public static class WorkOrderCompletionDocFileRules
|
|
{
|
|
private const string PdfContentType = "application/pdf";
|
|
|
|
private static readonly HashSet<string> AllowedExtensions =
|
|
new(StringComparer.OrdinalIgnoreCase) { ".pdf" };
|
|
|
|
// A browser sets the multipart part's Content-Type from File.type, which the OS
|
|
// sometimes leaves empty for a PDF. The completion-doc dialog already accepts
|
|
// that case on the client (a .pdf name with no type passes), so rejecting it
|
|
// here would fail uploads that work today. The %PDF- signature is the real gate.
|
|
private static readonly HashSet<string> UndeterminedContentTypes =
|
|
new(StringComparer.OrdinalIgnoreCase) { "application/octet-stream" };
|
|
|
|
public static bool IsAllowed(IFormFile file)
|
|
{
|
|
if (file == null || file.Length <= 0)
|
|
return false;
|
|
|
|
var declaredType = (file.ContentType ?? string.Empty).Trim();
|
|
if (!string.IsNullOrWhiteSpace(declaredType)
|
|
&& !declaredType.Equals(PdfContentType, StringComparison.OrdinalIgnoreCase)
|
|
&& !UndeterminedContentTypes.Contains(declaredType))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var extension = Path.GetExtension(file.FileName ?? string.Empty);
|
|
if (string.IsNullOrWhiteSpace(extension) || !AllowedExtensions.Contains(extension))
|
|
return false;
|
|
|
|
try
|
|
{
|
|
using var stream = file.OpenReadStream();
|
|
var headerLength = (int)Math.Min(Math.Max(file.Length, 0), 64);
|
|
if (headerLength == 0)
|
|
return false;
|
|
|
|
var header = new byte[headerLength];
|
|
var read = stream.Read(header, 0, header.Length);
|
|
if (read <= 0)
|
|
return false;
|
|
|
|
if (read < header.Length)
|
|
Array.Resize(ref header, read);
|
|
|
|
return WorkOrderMediaFileRules.MatchesSignature(PdfContentType, header);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static void EnsureAllowed(IFormFile file)
|
|
{
|
|
if (!IsAllowed(file))
|
|
{
|
|
throw new Exceptions.WorkOrderBoardValidationException(
|
|
"UnsupportedMediaType",
|
|
"The completion document must be a PDF file.");
|
|
}
|
|
}
|
|
}
|
|
}
|