{"id":50486966,"url":"https://github.com/systemslibrarian/secure-file-upload-dotnet","last_synced_at":"2026-06-01T23:03:02.017Z","repository":{"id":352965051,"uuid":"1217021630","full_name":"systemslibrarian/secure-file-upload-dotnet","owner":"systemslibrarian","description":"Production-grade 8-layer file upload security pipeline for ASP.NET Core 8, with an AI red-team adversarial security analysis. ","archived":false,"fork":false,"pushed_at":"2026-05-30T16:24:01.000Z","size":327,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-30T18:04:57.656Z","etag":null,"topics":["aspnet-core","csharp","defense-in-depth","dotnet","file-upload-","security"],"latest_commit_sha":null,"homepage":"https://systemslibrarian.github.io/secure-file-upload-dotnet/","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/systemslibrarian.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY-ANALYSIS.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-21T13:18:11.000Z","updated_at":"2026-05-30T16:23:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/systemslibrarian/secure-file-upload-dotnet","commit_stats":null,"previous_names":["systemslibrarian/secure-file-upload-dotnet"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/systemslibrarian/secure-file-upload-dotnet","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fsecure-file-upload-dotnet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fsecure-file-upload-dotnet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fsecure-file-upload-dotnet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fsecure-file-upload-dotnet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/systemslibrarian","download_url":"https://codeload.github.com/systemslibrarian/secure-file-upload-dotnet/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fsecure-file-upload-dotnet/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33797128,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aspnet-core","csharp","defense-in-depth","dotnet","file-upload-","security"],"created_at":"2026-06-01T23:03:00.670Z","updated_at":"2026-06-01T23:03:02.011Z","avatar_url":"https://github.com/systemslibrarian.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SecureFileUpload.Core\n\n**Defense-in-depth file upload pipeline for ASP.NET Core 8 / 9 / 10 — AES-256-GCM envelope encryption, Argon2id key derivation, deep content validation, and pluggable virus scanning.**\n\n[![NuGet](https://img.shields.io/nuget/v/SecureFileUpload.Core.svg?style=flat-square)](https://www.nuget.org/packages/SecureFileUpload.Core)\n[![NuGet downloads](https://img.shields.io/nuget/dt/SecureFileUpload.Core.svg?style=flat-square)](https://www.nuget.org/packages/SecureFileUpload.Core)\n[![Build](https://github.com/systemslibrarian/secure-file-upload-dotnet/actions/workflows/nuget-publish.yml/badge.svg)](https://github.com/systemslibrarian/secure-file-upload-dotnet/actions/workflows/nuget-publish.yml)\n[![Targets: net8.0 / net9.0 / net10.0](https://img.shields.io/badge/targets-net8.0%20%7C%20net9.0%20%7C%20net10.0-512BD4.svg?style=flat-square)](https://dotnet.microsoft.com/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE)\n\n`SecureFileUpload.Core` is a battle-tested file-upload pipeline lifted from a production ASP.NET Core document-intake workflow, de-branded, hardened, and shipped as a single NuGet package. Every layer is implemented in code you can read; every limitation is named in [`KNOWN-GAPS.md`](KNOWN-GAPS.md); every security claim traces to a specific line in `src/` per the audit in [`SECURITY-ANALYSIS.md`](SECURITY-ANALYSIS.md).\n\n\u003e *\"So whether you eat or drink or whatever you do, do it all for the glory of God.\"*\n\u003e — 1 Corinthians 10:31\n\n---\n\n## What's New in 3.0.2\n\n`3.0.2` restores **multi-targeting** for `net8.0`, `net9.0`, and `net10.0`. Dropping to `net10.0`-only in `3.0.0` was a strategic mistake — it forced consumers on currently-supported LTS / STS runtimes to either pin to the `2.x` line or upgrade their host before they could take any of the `3.x` hardening. This release re-publishes the same source as a multi-targeted package.\n\n- **TargetFrameworks: `net8.0;net9.0;net10.0`.** Same source, same crypto posture, same on-disk envelope formats. The entire pipeline already compiled against the .NET 8 BCL; no conditional compilation was needed.\n- **No behavioral or breaking changes from `3.0.1`.** Argon2id KEK derivation, AES-256-GCM v2 envelope, legacy PBKDF2 fallback, opaque download tokens, hardened download controller, and Data Protection deployment guidance are unchanged.\n- **AssemblyVersion stays at `3.0.0.0`** so this is a drop-in upgrade with no binding-redirect change required.\n\n## What's New in 3.0.0\n\n`3.0.0` is the hardened download-surface release. **The 8-layer upload pipeline and on-disk crypto formats are unchanged from `2.0.0`.** This release is about the staff-download contract, release validation, and operator-facing correctness.\n\n- **Opaque download tokens replace path-based download links.** The reference controller now accepts `fileToken`, issued by `IFileAccessTokenService`, instead of a storage-relative path. If you linked staff downloads with `relativePath`, update that integration before upgrading.\n- **Release validation is now an actual gate.** The solution tests and the runtime smoke harness both run in CI before pack/publish, so the NuGet package is validated against the same path documented in this repo.\n- **Scanner outage logs now match runtime behavior.** ClamAV and Windows Defender unavailability are logged as `NotScanned` fail-open conditions instead of incorrectly implying fail-closed rejection.\n\n`2.0.0` was the first stable release of the modernized line. The Argon2id KEK and PBKDF2 fallback story from that release still applies:\n\n- **Argon2id for KEK derivation.** The master Key Encryption Key is now derived via Argon2id (RFC 9106, OWASP 2024+ recommendation) with memory-hard defaults — `m=64 MiB, t=3, p=4`. Memory-hardness raises the cost-per-guess on GPUs and ASICs by orders of magnitude over the prior PBKDF2-SHA256 derivation.\n- **Backward-compatible online upgrade.** Files wrapped under prior PBKDF2 KEKs (600 000 and 210 000 iterations) still decrypt via `FileUpload:KeyDerivation:LegacyKekFallback=true` (default). No file on disk is bricked by the upgrade. New writes always use the Argon2id-derived KEK.\n- **Configurable KDF.** Argon2id is the default; `KeyDerivation:Algorithm = \"Pbkdf2\"` is available for FIPS-restricted environments. All Argon2id parameters and the PBKDF2 iteration count are tunable from `appsettings.json`.\n- **Packaging.** Deterministic build, Source Link, `.snupkg` symbols, and `README.md` / `LICENSE` / `SECURITY-ANALYSIS.md` / `KNOWN-GAPS.md` bundled inside the package itself.\n\nThe crypto posture, parameters, and honest residual risks are documented in [Implementation \u0026 Crypto Posture](#implementation--crypto-posture) below and in [`SECURITY-ANALYSIS.md`](SECURITY-ANALYSIS.md).\n\n---\n\n## Why this exists\n\nFile upload is one of the most consistently mishandled surfaces in web development. Most tutorials show you how to *receive* a file. Very few defend against:\n\n- Polyglot files (a valid JPEG that is also a working PHP shell)\n- Double-extension attacks (`photo.pdf.exe`)\n- MIME spoofing and magic-byte forgery\n- Path traversal via filename manipulation\n- PDF JavaScript injection (including inside FlateDecode-compressed object streams)\n- ZIP-bomb / pixel-flood attacks via image decoding\n- Log poisoning via crafted filenames\n- Disk exhaustion via batched uploads\n- Direct web-serving of attacker-controlled bytes\n\nThis package addresses every item on that list in code, then names the gaps it does *not* close. The red-team review in [`SECURITY-ANALYSIS.md`](SECURITY-ANALYSIS.md) traces each claim to its source line.\n\n---\n\n## The 8-layer pipeline\n\nEvery uploaded file passes through eight serial layers. **Failure at any content-decision layer rejects the file.** The pipeline is fail-closed on content; the single fail-open seam is virus-scanner *availability* (Layer 7), and that is documented, counted, and never silently relabelled as clean — see [`KNOWN-GAPS.md §Gap 9`](KNOWN-GAPS.md).\n\n```\n┌──────────────────────────────────────────────────────────────────────────┐\n│                         INCOMING FILE UPLOAD                              │\n└──────────────────────────────┬───────────────────────────────────────────┘\n                               │\n       ┌───────────────────────▼────────────────────────┐\n       │  Layer 1   File size (per-file + batch total)   │\n       │            Minimum size per format               │\n       └───────────────────────┬────────────────────────┘\n       ┌───────────────────────▼────────────────────────┐\n       │  Layer 2   Extension allowlist                   │\n       │            .jpg .jpeg .png .webp .pdf            │\n       └───────────────────────┬────────────────────────┘\n       ┌───────────────────────▼────────────────────────┐\n       │  Layer 3   MIME ↔ extension cross-validation     │\n       │            Browser MIME must match extension     │\n       └───────────────────────┬────────────────────────┘\n       ┌───────────────────────▼────────────────────────┐\n       │  Layer 4   Magic-byte signature check            │\n       │            JPEG / PNG / WebP fourCC / PDF        │\n       └───────────────────────┬────────────────────────┘\n       ┌───────────────────────▼────────────────────────┐\n       │  Layer 5   Filename inspection                   │\n       │            Double-extension, Unicode bidi,       │\n       │            path traversal, Windows reserved      │\n       └───────────────────────┬────────────────────────┘\n       ┌───────────────────────▼────────────────────────┐\n       │  Layer 6   Deep content validation               │\n       │            JPEG/PNG/WebP structural walkers,     │\n       │            PDF byte-pattern scan,                │\n       │            FlateDecode-compressed PDF stream     │\n       │            decompression and re-scan             │\n       └───────────────────────┬────────────────────────┘\n       ┌───────────────────────▼────────────────────────┐\n       │  Layer 7   Virus scan (pluggable)                │\n       │            Windows Defender OR ClamAV / clamd    │\n       │            Detection fail-closed,                │\n       │            Availability fail-open (tracked)      │\n       └───────────────────────┬────────────────────────┘\n       ┌───────────────────────▼────────────────────────┐\n       │  Layer 8   Encrypted storage                     │\n       │            AES-256-GCM envelope (v2):            │\n       │              per-file random DEK                 │\n       │              wrapped under Argon2id-derived KEK  │\n       │            Image recompression strips polyglot   │\n       │            tails before encryption.              │\n       │            Randomized filename, outside wwwroot. │\n       │            Path traversal re-checked before      │\n       │            write via PathHelper.IsPathUnderBase. │\n       └──────────────────────────────────────────────────┘\n```\n\n---\n\n## Install\n\n```bash\ndotnet add package SecureFileUpload.Core\n```\n\nMulti-targets **`net8.0`**, **`net9.0`**, and **`net10.0`** — same source, same crypto posture, on every currently-supported .NET runtime. The package references `Microsoft.AspNetCore.App` as a framework reference, so nothing extra ships inside it; your runtime's existing ASP.NET Core does the heavy lifting.\n\nPick the TFM that matches your host:\n\n| Host runtime              | NuGet picks         | Notes |\n|---------------------------|---------------------|-------|\n| .NET 8 (LTS, in support)  | `net8.0` build      | Same `FileUploadService` / `FileContentValidator` source. |\n| .NET 9 (STS, in support)  | `net9.0` build      | Same source. |\n| .NET 10 (LTS, current)    | `net10.0` build     | Same source. |\n\n\u003e If you're upgrading from `3.0.0` / `3.0.1`, this is a drop-in change — `AssemblyVersion` is unchanged at `3.0.0.0` and the on-disk envelope formats (`ENCGCM\\0\\x01` / `ENCGCM\\0\\x02`) are byte-for-byte compatible. No re-wrap, no migration.\n\n---\n\n## Quick start\n\n### 1. Register the services\n\n```csharp\n// Program.cs\nusing SecureFileUpload.Services;\n\nbuilder.Services.AddSecureFileUpload();\n\n// Match this to FileUpload:MaxTotalUploadBytes in appsettings.json.\nbuilder.Services.Configure\u003cFormOptions\u003e(options =\u003e\n{\n    options.MultipartBodyLengthLimit = 53_477_376; // 51 MB\n});\n```\n\n`AddSecureFileUpload()` registers `FileContentValidator`, the platform-appropriate `IVirusScanService` (Windows Defender on Windows, ClamAV elsewhere), `IFileUploadService`, and `IFileAccessTokenService` in one call. The scanner backend is picked at startup; download tokens are issued through ASP.NET Core Data Protection with a short lifetime by default.\n\n### 2. Receive an upload\n\n```csharp\n[HttpPost]\n[RequestSizeLimit(53_477_376)]\npublic async Task\u003cIActionResult\u003e Submit(MyInputModel model)\n{\n    if (!ModelState.IsValid)\n        return View(model);\n\n    var result = await _fileUploadService.UploadFilesAsync(\n        Request.Form.Files, model.LastName, \"intake\");\n\n    if (!result.Success)\n    {\n        // result.Errors are user-safe; result.WorkflowOutcome is\n        // AllSaved | PartialSaved | AllRejected | NoFiles.\n        foreach (var msg in result.Errors)\n            ModelState.AddModelError(string.Empty, msg);\n        return View(model);\n    }\n\n    return RedirectToAction(nameof(Submitted), new { id = result.SubmissionFolder });\n}\n```\n\n### 3. Serve a file safely\n\n```csharp\nusing SecureFileUpload.Services;\n\nbuilder.Services\n  .AddAuthentication(\"Cookies\")\n  .AddCookie(\"Cookies\");\n\nbuilder.Services.AddAuthorization(options =\u003e\n{\n  options.AddPolicy(\"StaffFiles\", policy =\u003e\n  {\n    policy.RequireAuthenticatedUser();\n    policy.RequireRole(\"Staff\");\n    // Add your own MFA / claim requirements here.\n  });\n});\n\n// SecureFileDownloadController is [Authorize] by default. Apply your stricter\n// staff-only policy at the endpoint layer so the sample policy is actually used.\nbuilder.Services.AddControllers().AddApplicationPart(typeof(SecureFileDownloadController).Assembly);\n\napp.UseAuthentication();\napp.UseAuthorization();\napp.MapControllers().RequireAuthorization(\"StaffFiles\");\n```\n\n### 4. Issue an opaque download token\n\n```csharp\npublic sealed class StaffFilesController : Controller\n{\n    private readonly IFileAccessTokenService _fileAccessTokenService;\n\n    public StaffFilesController(IFileAccessTokenService fileAccessTokenService)\n    {\n        _fileAccessTokenService = fileAccessTokenService;\n    }\n\n    public IActionResult DownloadFirst(FileUploadResult result)\n    {\n        string token = _fileAccessTokenService.CreateToken(result.UploadedFilePaths[0]);\n        string url = $\"/staff/files/download?fileToken={Uri.EscapeDataString(token)}\";\n        return Redirect(url);\n    }\n}\n```\n\nThe token is opaque, signed, and short-lived by default. Staff-facing URLs never need to expose a storage-relative path. If your application has both public and staff-only controllers, scope the `RequireAuthorization(\"StaffFiles\")` call to the staff route set instead of every controller endpoint in the app.\n\nA complete `appsettings.json` reference is in [Configuration](#configuration) below.\n\n---\n\n## Deployment notes\n\n### Data Protection and multi-instance deployments\n\n`AddSecureFileUpload()` calls `services.AddDataProtection()` so that `IFileAccessTokenService` can sign download tokens. With the default registration, **each process generates its own ephemeral key ring** — fine for a single-instance app, but **broken across replicas**: a token issued by node A will not validate on node B, so any load-balanced staff request to `/staff/files/download` has a chance of returning `400 Invalid file reference.`\n\nFor any deployment with more than one instance (Kubernetes, multiple App Service workers, an autoscaled VM scale set, dev → staging container, blue-green), configure Data Protection to share a key store and pin an application name **before** `AddSecureFileUpload()`:\n\n```csharp\n// Pick ONE persistence backend that all instances can read.\n\n// Shared filesystem mount (Linux + ReadWriteMany PVC, Windows file share):\nbuilder.Services.AddDataProtection()\n    .PersistKeysToFileSystem(new DirectoryInfo(\"/var/keys/secure-file-upload\"))\n    .SetApplicationName(\"SecureFileUpload\");\n\n// — OR — Azure Blob + Key Vault (recommended on Azure):\n// builder.Services.AddDataProtection()\n//     .PersistKeysToAzureBlobStorage(blobUri, credential)\n//     .ProtectKeysWithAzureKeyVault(keyIdentifier, credential)\n//     .SetApplicationName(\"SecureFileUpload\");\n\nbuilder.Services.AddSecureFileUpload();\n```\n\n`SetApplicationName(...)` is the gate that makes keys interchangeable across processes — without it, ASP.NET Core derives a per-content-root application discriminator and tokens still won't cross instances even with a shared key store. The string itself is not a secret; just keep it stable across deployments.\n\n**Symptom of getting this wrong:** intermittent `DOWNLOAD_REJECTED_BAD_TOKEN` warnings in the logs, only on multi-instance environments, only for tokens issued by a different instance than the one handling the download. The single-instance happy-path keeps working, which makes it easy to ship the misconfiguration. Catch it in load-balanced staging.\n\n### Token replay window\n\nA signed download token is reusable for its configured lifetime (`FileDownload:TokenLifetimeMinutes`, default 15 minutes). If a token leaks via a referrer, a screen recording, a log entry, or shared-screen support, an attacker with network access to the staff endpoint can replay it until expiry. Mitigations layered into the library and the recommended deployment:\n\n- `Cache-Control: no-store, no-cache, must-revalidate, private` on every download response — no shared proxy keeps a copy.\n- `Referrer-Policy: no-referrer` so the token doesn't leak to other origins.\n- `[Authorize]` on `SecureFileDownloadController` plus the recommended `RequireAuthorization(\"StaffFiles\")` policy — a leaked token is useless to an unauthenticated attacker.\n- Short default lifetime; lower it further for high-sensitivity workflows.\n\nThere is no single-use / nonce-redemption mode in v3. If you need one, track it as a v3.x feature request — the right shape is a redemption store keyed by token hash, gated by `IFileAccessTokenService`.\n\n---\n\n## Implementation \u0026 Crypto Posture\n\nThis section names primitives, parameters, and residual risks. It is the single source of truth for the cryptographic posture; the marketing tagline is at the top of this README.\n\n| Aspect | Implementation | Notes |\n|---|---|---|\n| Symmetric encryption | **AES-256-GCM** via `System.Security.Cryptography.AesGcm` | 96-bit nonce, 128-bit auth tag — NIST SP 800-38D / RFC 5288. |\n| Encryption scheme | **Envelope (v2)** — per-file random 256-bit DEK wrapped under a master KEK | KEK rotation rewraps DEKs without re-encrypting file payloads. |\n| KEK derivation (writes) | **Argon2id** via `Konscious.Security.Cryptography.Argon2` 1.3.x | RFC 9106; OWASP 2024+ recommendation. Memory-hard. |\n| Argon2id parameters (defaults) | `m=64 MiB`, `t=3`, `p=4`, fixed application salt | Above OWASP server-side minimum; targets ~250–500 ms derivation on a modern x64 core. |\n| KEK derivation (decrypt fallback) | PBKDF2-SHA256, 600 000 and 210 000 iterations | Tried during decryption when `LegacyKekFallback=true`; never for new writes. |\n| RNG | `RandomNumberGenerator` (CSPRNG) for DEKs, nonces, filename suffixes | No `System.Random`, no `Guid.NewGuid()`, no `DateTime.Ticks` in security paths. |\n| Storage format markers | `ENCGCM\\0\\x01` (legacy single-key) / `ENCGCM\\0\\x02` (current envelope) | Layout: `marker‖dek_nonce‖dek_tag‖wrapped_dek‖file_nonce‖file_tag‖ciphertext`. |\n| Buffer hygiene | Plaintext, DEK, and password buffers zeroed via `CryptographicOperations.ZeroMemory` | Reduces in-memory exposure window. Not a guarantee against GC copies. |\n| Startup guards | `EncryptionEnabled=true` + missing/placeholder secret ⇒ `InvalidOperationException` | Misconfigurations fail loudly at deploy time, not silently at runtime. |\n| FIPS posture | **Not FIPS-validated.** Argon2id is not in FIPS 140-3 ASMs as of 2026 | Opt into `KeyDerivation:Algorithm = \"Pbkdf2\"` for FIPS-only deployments. |\n| TLS / transport | Not provided by this library | Enforce HSTS and HTTPS at Kestrel / reverse-proxy level. |\n| At-rest device encryption | Not provided by this library | BitLocker / LUKS / dm-crypt strongly recommended on the storage volume. |\n\n### Honest limitations\n\n- **The KDF salt is in source.** It is identical across deployments of this library version. The protection model assumes the *secret* lives in a real secrets manager (Key Vault, AWS Secrets Manager, env var injected by the platform) — never `appsettings.json` committed to a repo.\n- **Argon2id is not FIPS-validated.** Compliance-bound deployments must select `Pbkdf2` explicitly.\n- **The KEK lives in process memory.** A memory-disclosure or core-dump capability on the host bypasses the KDF entirely. Mitigations are deployment-level (least privilege, ASLR, sealed VMs, confidential compute).\n- **No HSM / KMS integration in v1.** A KMS-backed KEK is tracked in [`docs/hardening-roadmap.md`](docs/hardening-roadmap.md) as the right next step for high-assurance deployments.\n- **Argon2id parameters are CPU/RAM-bound.** On a constrained container, startup derivation may take longer than the ~250–500 ms target. The library logs `KDF_ARGON2ID_DERIVED | ElapsedMs=...` so this is measurable.\n\nFor the full code-traced security review, see [`SECURITY-ANALYSIS.md`](SECURITY-ANALYSIS.md). For things this pipeline does *not* protect against, see [`KNOWN-GAPS.md`](KNOWN-GAPS.md).\n\n---\n\n## Configuration\n\n```json\n{\n  \"FileUpload\": {\n    \"StorageRoot\": \"../uploads\",\n    \"MaxFileSizeBytes\": 10485760,\n    \"MaxFileCount\": 5,\n    \"MaxTotalUploadBytes\": 52428800,\n    \"MinStorageFreeBytes\": 536870912,\n    \"MinTempFreeBytes\": 536870912,\n    \"LowDiskWarningBytes\": 2147483648,\n    \"RecompressImages\": true,\n    \"JpegRecompressQuality\": 95,\n    \"EncryptionEnabled\": false,\n    \"EncryptionSecret\": \"CHANGE_THIS_TO_A_REAL_SECRET_MINIMUM_32_CHARS\",\n    \"KeyDerivation\": {\n      \"Algorithm\": \"Argon2id\",\n      \"Argon2id\": {\n        \"MemoryKiB\": 65536,\n        \"Iterations\": 3,\n        \"Parallelism\": 4\n      },\n      \"Pbkdf2\": {\n        \"Iterations\": 600000\n      },\n      \"LegacyKekFallback\": true\n    }\n  },\n  \"FileContent\": {\n    \"InspectCompressedPdfStreams\": true,\n    \"MaxCompressedStreamsToInspect\": 64,\n    \"MaxDecompressedStreamBytes\": 16777216,\n    \"RejectEncryptedPdfs\": true,\n    \"RejectInteractivePdfs\": false,\n    \"MaxImageWidth\": 10000,\n    \"MaxImageHeight\": 10000,\n    \"MaxImagePixels\": 40000000\n  },\n  \"FileDownload\": {\n    \"TokenLifetimeMinutes\": 15\n  },\n  \"VirusScan\": {\n    \"Enabled\": false,\n    \"WindowsDefender\": {\n      \"MpCmdRunPath\": \"C:\\\\Program Files\\\\Windows Defender\\\\MpCmdRun.exe\",\n      \"TempScanPath\": \"C:\\\\Temp\\\\VirusScan\",\n      \"TimeoutSeconds\": 30\n    },\n    \"ClamAv\": {\n      \"Host\": \"localhost\",\n      \"Port\": 3310,\n      \"TimeoutSeconds\": 30,\n      \"MaxStreamBytes\": 26214400\n    }\n  }\n}\n```\n\n| Setting | Purpose |\n|---|---|\n| `FileUpload:StorageRoot` | Resolved relative to `ContentRootPath`. Must land outside `wwwroot`. The service refuses to start otherwise. |\n| `FileUpload:EncryptionSecret` | ≥ 32 chars, must not contain `CHANGE_THIS`. Store in a secrets manager — **never** in checked-in config. |\n| `FileUpload:KeyDerivation:Algorithm` | `Argon2id` (default) or `Pbkdf2` (FIPS-restricted environments only). |\n| `FileUpload:KeyDerivation:Argon2id:*` | Tune for your CPU/RAM budget. Library logs derivation time at startup. |\n| `FileUpload:KeyDerivation:LegacyKekFallback` | `true` (default) keeps PBKDF2 fallback KEKs available for *decryption only*. Set `false` after every file has been re-wrapped. |\n| `FileUpload:RecompressImages` | `true` (default) strips polyglot tails by re-encoding JPEG/PNG/WebP through ImageSharp. |\n| `FileDownload:TokenLifetimeMinutes` | Lifetime for opaque download tokens issued by `IFileAccessTokenService`. Default 15 minutes; max 24 hours. |\n| `VirusScan:Enabled` | When `false`, Layer 7 is bypassed. Layers 1–6 + 8 still run. |\n| `VirusScan:ClamAv:MaxStreamBytes` | Must align with `StreamMaxLength` in your `clamd.conf`. |\n\n---\n\n## Dependencies\n\nDeclared by the NuGet package — no manual installation required:\n\n- **ASP.NET Core 10+** shared framework (via `FrameworkReference`)\n- **[SixLabors.ImageSharp 3.1.x](https://github.com/SixLabors/ImageSharp)** — image identification and polyglot-tail recompression\n- **[Konscious.Security.Cryptography.Argon2 1.3.x](https://github.com/kmaragon/Konscious.Security.Cryptography)** — Argon2id KEK derivation\n\nRequired at runtime if `VirusScan:Enabled=true` (not NuGet packages):\n\n- **Windows Defender** (`MpCmdRun.exe`) — Windows only, used by `WindowsDefenderScanService`\n- **ClamAV** (`clamd` listening on TCP) — Linux / macOS / containers, used by `ClamAvScanService`\n\nThe scanner is selected automatically by `AddSecureFileUpload()` based on `OperatingSystem.IsWindows()`.\n\n---\n\n## Source layout\n\n| File | Responsibility |\n|---|---|\n| `src/FileUploadService.cs` | Orchestrates the 8-layer pipeline. Batch limits, capacity checks, image recompression (Gap 1), envelope encryption (v2), Argon2id KEK derivation with PBKDF2 fallback, log-poisoning-safe filename handling. |\n| `src/FileContentValidator.cs` | Layer 6 deep validation. JPEG / PNG / WebP structural walking, PDF pattern scan, FlateDecode-compressed PDF stream inspection (Gap 2). Fail-closed on unknown types. |\n| `src/WindowsDefenderScanService.cs` | Layer 7 — Windows Defender `MpCmdRun.exe`. Secure-delete (zero-before-delete) of temp files. |\n| `src/ClamAvScanService.cs` | Layer 7 — `clamd` over TCP using `zINSTREAM`. No temp file is written. Cross-platform. |\n| `src/FileAccessTokenService.cs` | Opaque, signed, time-limited download tokens backed by ASP.NET Core Data Protection. |\n| `src/SecureFileDownloadController.cs` | Reference hardened download surface. Tokenized file reference, `Content-Disposition: attachment`, strict CSP, COOP/COEP/CORP, re-checks resolved path traversal. |\n| `src/DependencyInjection/SecureFileUploadServiceCollectionExtensions.cs` | `AddSecureFileUpload()` one-liner DI registration. |\n| `src/Utilities/PathHelper.cs` | Canonicalized `IsPathUnderBase` — defeats the `string.StartsWith(\"/uploads\")` prefix-confusion bug. |\n| `tests/Fuzz/` | SharpFuzz + AFL++ harness for `FileContentValidator.ValidateAsync`. |\n| `tests/SmokeTest/` | Runtime smoke test — Argon2id round-trip, v2 envelope, legacy PBKDF2 fallback, misconfig guard. Executed in CI and runnable locally with `dotnet run --project tests/SmokeTest -c Release`. |\n\n---\n\n## Docs\n\n- [`SECURITY-ANALYSIS.md`](SECURITY-ANALYSIS.md) — full code-traced security review, with each claim pointing at source lines\n- [`KNOWN-GAPS.md`](KNOWN-GAPS.md) — honest limitations and what this does NOT protect against\n- [`docs/threat-model.md`](docs/threat-model.md) — what attack each layer defeats\n- [`docs/hardening-roadmap.md`](docs/hardening-roadmap.md) — recommended next steps toward the strongest realistic posture\n- [`tests/attack-vectors.md`](tests/attack-vectors.md) — per-layer attack test cases\n- [`tests/Fuzz/`](tests/Fuzz/) — SharpFuzz + AFL++ harness for the deep content validator\n\n---\n\n## Release process\n\nPublishing is handled by GitHub Actions in `.github/workflows/nuget-publish.yml`.\n\n- Push to `main` runs library build, solution tests, the runtime smoke harness, pack, and a fuzz-harness build — no publish.\n- Push a `v*` tag to publish to NuGet.org. The workflow derives the package version from the tag (`v2.0.0` → `2.0.0`).\n- `--skip-duplicate` is used, so re-running on an existing version is non-destructive.\n\n```bash\ngit tag v2.0.0\ngit push origin v2.0.0\n```\n\nThe `\u003cVersion\u003e` in `src/SecureFileUpload.Core.csproj` is the source of truth for local packs and CI artifacts; tag builds override it for the published NuGet version.\n\n---\n\n## Contributing\n\nIssues and PRs welcome — especially:\n\n- Unit-test coverage for the per-layer validation paths\n- Additional format validators (GIF, BMP deep content validation)\n- Async / queued virus-scan worker for higher-volume deployments\n- KMS / HSM-backed KEK provider\n\nIf you're filing a security report, please open a private advisory on GitHub rather than a public issue.\n\n---\n\n## License\n\n[MIT](LICENSE). Use freely. Attribution appreciated, not required.\n\n---\n\n\u003e *\"So whether you eat or drink or whatever you do, do it all for the glory of God.\"*\n\u003e — 1 Corinthians 10:31\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsystemslibrarian%2Fsecure-file-upload-dotnet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsystemslibrarian%2Fsecure-file-upload-dotnet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsystemslibrarian%2Fsecure-file-upload-dotnet/lists"}