English · Português
A small file-validation microservice: hand it an uploaded file and it tells you whether the file is malicious or not — designed to sit in front of existing apps with a single HTTP call before the file reaches storage.
Most upload pipelines trust the file extension. Malicious uploads — PDFs with auto-running JavaScript, Office documents with DDE/macros, CSV formula injection, polyglot images — slip past extension checks, and past signature-based antivirus when the payload is new. FileScan catches that class of attack at the application layer, before the file is stored. It also fills a real gap: there is no widely-used free .NET library for multi-format injection detection — the alternatives are commercial CDR products or language-specific tools.
- Multi-format injection detection — PDF JavaScript, Office DDE/macros, CSV formula injection
(OWASP), polyglot images — plus recursive inspection of PDF attachments (a benign embedded XML
passes; an embedded
.exeis caught). - True file-type checking by content / magic bytes (Mime-Detective), not just the extension.
- No container required — the ClamAV antivirus layer is optional; with it off, the service is pure .NET and deploys like any ordinary web app.
- Validated on real documents — zero false positives — dozens of real-world files (insurance PDFs, Office documents, images) pass cleanly after false-positive tuning.
- Security-minded — fail-closed semantics, per-client rate limiting on by default, Swagger gated to Development, optional constant-time API-key auth, configurable size/decompression limits.
- 36 automated tests (xUnit) with inputs generated in code —
dotnet test, no Docker needed.
⚠️ Notice / Scope: FileScan performs heuristic detection of malicious / script-injection content. It is not a certified CDR product, it does not replace a full antivirus or a commercial solution, and it is provided without warranty (see LICENSE). Use it as a defense-in-depth layer and validate it in your own context — obfuscated/encrypted payloads and zero-day threats may evade it. See SECURITY.md.
Three validation layers, in order:
- Structural (cheap, synchronous): size, extension allowlist, and real content type via
Mime-Detective (magic bytes) — rejects dangerous binaries (a disguised
.exe) and files whose content doesn't match the declared extension (e.g. a PNG renamed to.pdf). - Active content (multi-format heuristics): detects script injection per file type —
- PDF: JavaScript (
/JavaScript,/JS),/Launch, and recursive inspection of attachments (/EmbeddedFile— the attachment is extracted and validated; benign passes, an embedded exe/script/macro is caught). Covers FlateDecode streams and normalizes hex-encoded names (/J#53≡/JS) so they can't evade detection. - Office OOXML (
docx/xlsx): unzips and looks for DDE, macros (vbaProject), formula injection, and OLE objects. - CSV: formula/command injection per OWASP (cell starting with
=@Tab, or+/-when it looks like a formula;cmd|,WEBSERVICE…). - Images (
jpg/png): embedded<script>/<?phpand data appended after the image end (polyglot). - Legacy/HTML (
doc/xls):<script>, DDE, formulas, and macro markers.
- PDF: JavaScript (
- Antivirus (optional): scan via ClamAV (open-source engine) using the
nClamclient.
The active-content layer detects and applies a policy — it does not sanitize (not CDR). Encrypted/obfuscated payloads may evade it; full coverage requires CDR or a sandbox.
ClamAV is optional (
FileScan:ClamAv:Enabled): when disabled, the service runs only the structural + active-content layers — no container/daemon required.
- Request:
multipart/form-data, file in thefilefield. - Auth:
X-Api-Keyheader (only whenFileScan:ApiKeyis configured). - Response 200 (final verdict):
{ "fileName": "contract.pdf", "sizeBytes": 18342, "verdict": "Clean", // Clean | Malicious | Rejected "reason": null, // populated when Malicious/Rejected "engine": "clamav", // "clamav" or "filescan" (which layer decided) "scannedAtUtc": "2026-05-29T13:00:00.0000000Z" } - Response 503:
verdict = "Error"— the file could not be scanned (ClamAV down). The caller must fail closed. (Only happens whenClamAv:Enabled=true.)
Caller's golden rule: only persist the file if HTTP 200 and verdict == "Clean".
⚠️ Clean≠ "safe to render inline". FileScan only scans; it never serves files. When you serve a user upload to a browser, the consuming app must serve it as a download (Content-Disposition: attachment), disable sniffing (X-Content-Type-Options: nosniff), send the correctContent-Type, and apply a CSP (object-src 'none') — ideally from a separate, cookie-less origin. That is what neutralizes stored XSS via JavaScript-bearing PDFs. See SECURITY.md.
Liveness — the process is up.
Readiness — ClamAV answers a ping (200) or not (503). Always 200 when ClamAV is disabled.
Interactive docs (Swagger UI) are served at /swagger.
| Key | Default | Description |
|---|---|---|
MaxFileSizeBytes |
26214400 (25 MB) |
Maximum accepted file size (also drives the request body limit, plus a small margin) |
MaxDecompressedBytesPerStream |
16777216 (16 MB) |
Per-stream/attachment cap on decompressed bytes (zip-bomb guard) |
AllowedExtensions |
pdf,doc,docx,xls,xlsx,csv,jpg,jpeg,png |
Accepted extension allowlist; empty = no restriction |
ApiKey |
"" |
Requires the X-Api-Key header when set |
ClamAv:Enabled |
true |
Enables the antivirus layer. false = structural + active-content only (no container/daemon) |
ClamAv:Host / ClamAv:Port |
localhost / 3310 |
Address of the clamd daemon (when enabled) |
ActiveContent:OnDetected |
Reject |
Active content (PDF/Office/CSV/images): Reject, Flag (passes + warnings), or Ignore |
RateLimit:Enabled / :PermitLimit / :WindowSeconds |
true / 60 / 60 |
Rate limit on /scan per client (API key, else IP): N requests per window → 429 |
Any key can be overridden by environment variables, e.g. FileScan__ClamAv__Enabled=false.
Without ClamAV (structural + active-content only — no Docker):
FileScan__ClamAv__Enabled=false dotnet run --project FileScan.Api
# then open http://localhost:5080/swaggerWith the full antivirus layer:
docker run -d --name clamav -p 3310:3310 clamav/clamav # wait until "healthy"
dotnet run --project FileScan.Apiusing var content = new MultipartFormDataContent();
content.Add(new StreamContent(file.OpenReadStream()), "file", file.FileName);
var resp = await httpClient.PostAsync("https://filescan.../scan", content, ct);
if (resp.StatusCode != HttpStatusCode.OK)
throw new InvalidOperationException("Validation unavailable — upload refused."); // fail closed
var result = await resp.Content.ReadFromJsonAsync<ScanResponse>(cancellationToken: ct);
if (result!.Verdict != "Clean")
throw new InvalidOperationException($"File refused: {result.Reason}");
// only here do you write to storagedotnet testAutomated xUnit suite: per-format inspection + end-to-end tests against the /scan endpoint
(with ClamAV disabled, so no Docker is needed). Test inputs are generated in code — no external
files. There are also manual helper scripts under _testfiles/ (run_pdf_batch.py <folder>,
make-injections.ps1).
MIT © 2026 Vitor Fallavena.
Dependencies: nClam (Apache-2.0), Mime-Detective (MIT; Default definitions free for commercial use), Serilog (Apache-2.0), Swashbuckle (MIT). ClamAV (GPLv2) runs as a separate process/container — it is not linked into this project's code.
Designed and built with Claude Code — from the initial analysis through the multi-format detection, false-positive tuning on real documents, security hardening, and tests.