FileScan performs heuristic detection of malicious / script-injection content in uploaded files. It is not a certified CDR (Content Disarm & Reconstruction) product and is not a replacement for a full antivirus or a certified commercial solution.
- It is a defense-in-depth layer, not a guarantee.
- Obfuscated/encrypted payloads and zero-day threats may evade detection.
- The optional ClamAV layer detects known malware only (signature-based).
- The software is provided without warranty (see LICENSE).
Always validate behavior in your own environment before relying on it.
FileScan is scan-only: it inspects an upload and returns a verdict — it never
serves files back. For PDF/OOXML, Clean means the supported structural/active-content
inspection completed with no finding, warning, skipped work, exhausted budget or ambiguity.
It still does not mean "malware-free" or "safe to render inline in a browser".
For the class of attack where a PDF (or other document) carries auto-running JavaScript aimed at stored XSS, the strongest mitigations happen at serving time, in the consuming application — FileScan cannot apply them. When you serve a user-uploaded file to a browser, you must:
- Serve it as a download, not inline:
Content-Disposition: attachment; filename="...". - Disable MIME sniffing:
X-Content-Type-Options: nosniff. - Send the correct, explicit
Content-Type(e.g.application/pdf) — do not let the browser guess. - Apply a restrictive Content-Security-Policy on the route that renders/embeds uploads,
e.g.
Content-Security-Policy: script-src 'self'; object-src 'none'. - Prefer serving user uploads from a separate origin/sandbox domain (no session cookies) so that even a rendered payload cannot reach the main app's context.
Treating a Clean verdict as a license to render the file inline on the application's
own origin re-introduces the stored-XSS risk that FileScan reduced.
Trailing bytes after a PDF's final %%EOF. Nothing is tolerated: after the last startxref
only <offset>, %%EOF and whitespace are accepted, anything else (HTML, a second %%EOF, loose
objects, UTF-16 text) is NotInspected. A textual scan of such a tail can never end in Clean.
The same holds inside the file: every non-whitespace/non-comment byte must belong to an indirect
object, an xref table/trailer or a startxref line (byte-coverage check).
0.2.0 and earlier: denial of service of the host process. A crafted PDF (cyclic or very deep
/Names /Dests /Kids name tree, very deep page tree, cycles/long chains of indirect-reference aliases)
makes PdfPig 0.1.16 overflow the stack inside PdfDocument.Open; a .NET stack overflow is not
catchable, so the process hosting FileScan dies. Fixed in 0.3.0 by a guard that runs before Open.
0.3.0 was intermediate and its pre-open guard is best effort. It blocks the trivial attack but a file
built on purpose can still crash the hosting process. 0.4.0 runs PdfDocument.Open in a separate worker process
by default (FileScannerOptions.Isolation = Worker): a crash, OOM, timeout or invalid answer from the worker kills
that worker, starts a new one and returns NotInspected (never Clean) while the host survives.
Do not use Isolation = InProcess with hostile PDFs (tests and environments that cannot spawn processes only).
CI must gate on the exit code of dotnet test: the "Passed! N tests" summary printed before an abort can
mislead anyone who reads only the text.
O FileScan apenas escaneia: ele inspeciona o upload e devolve um veredito — nunca
serve o arquivo. Para PDF/OOXML, Clean significa que a inspeção estrutural/conteúdo ativo
suportada terminou sem achado, warning, trabalho pulado, orçamento esgotado ou ambiguidade.
Ainda não significa "sem malware" nem "pode renderizar inline com segurança".
Para a classe de ataque em que um PDF (ou outro documento) carrega JavaScript de auto-execução visando stored XSS, as mitigações mais fortes acontecem no momento de servir o arquivo, no app consumidor — o FileScan não tem como aplicá-las. Ao entregar um arquivo enviado por usuário a um browser, você deve:
- Servir como download, não inline:
Content-Disposition: attachment; filename="...". - Desligar o MIME sniffing:
X-Content-Type-Options: nosniff. - Mandar o
Content-Typecorreto e explícito (ex.:application/pdf) — não deixar o browser adivinhar. - Aplicar uma Content-Security-Policy restritiva na rota que renderiza/embute uploads,
ex.:
Content-Security-Policy: script-src 'self'; object-src 'none'. - De preferência, servir uploads de usuário a partir de uma origem/domínio sandbox separado (sem cookies de sessão), para que mesmo um payload renderizado não alcance o contexto do app principal.
Tratar um veredito Clean como licença para renderizar o arquivo inline na própria
origem da aplicação reintroduz o risco de stored XSS que o FileScan reduziu.
PdfDocument.Open (PdfPig 0.1.16) is the one step whose failure mode — an uncatchable stack overflow — kills the
process. By default (FileScannerOptions.Isolation = Worker) it runs in FileScan.Worker, a child process
shipped inside the NuGet package. What this gives you, and what it does not:
- Enforced: a worker that crashes, runs out of memory, exceeds
WorkerTimeout(20 s), answers invalid JSON, an incoherent result or a truncated stream is killed (whole process tree, only the PID the library started), replaced on the next scan, and the scan returnsNotInspected— neverClean. Cancellation kills the worker and propagates. A worker that cannot be found (Reason:erro de configuração do isolamento) or started (nodotnethost, OS limits that cannot be applied, bad handshake: Reasono worker não pôde ser iniciado: <category>) also yieldsNotInspected, with the details (paths included) only in theOnIsolationEventnotification. There is no silent fallback to in-process parsing. - Windows: Job Object per worker —
KILL_ON_JOB_CLOSE, a single process (no children), a committed-memory cap, no crash dialog. - Best effort only: on Linux/macOS the memory limit is the GC heap hard limit (
DOTNET_GCHeapHardLimit) — native memory is not capped, and there is no CPU limit beyond the timeout; a parent watchdog and stdin EOF stop orphaned workers. Use container limits for hard guarantees. Within the 20 s timeout a hostile file can still consume a worker's CPU; a flood of hostile uploads reduces throughput (keep the rate limit) but cannot crash the host. - Not a sandbox: the worker runs with the host's identity and filesystem/network access. Isolation protects availability; it does not contain code execution in the worker. The 0.3.0 pre-Open guard remains the first layer inside the worker, and the residual gaps listed for it no longer take the host down.
- Deployment: the host must be allowed to start child processes and have the .NET 10 runtime. Azure App Service:
dedicated plan (B1+) or a Linux container; Free/Shared restrict child processes.
Isolation = InProcessis unsafe for hostile PDFs.
O PdfDocument.Open (PdfPig 0.1.16) é o passo cuja falha — um estouro de pilha não capturável — derruba o
processo. Por padrão (FileScannerOptions.Isolation = Worker) ele roda no FileScan.Worker, um processo filho
dentro do pacote NuGet. O que isso garante e o que não garante:
- Garantido: worker que cai, estoura memória, passa de
WorkerTimeout(20 s), responde JSON inválido, resultado incoerente ou fluxo truncado é morto (árvore de processos inteira, só o PID que a biblioteca iniciou), substituído no próximo scan, e o scan devolveNotInspected— nuncaClean. O cancelamento mata o worker e é propagado. Worker ausente (Reason:erro de configuração do isolamento) ou impossível de iniciar (sem hostdotnet, limites do SO não aplicáveis, handshake inválido: Reasono worker não pôde ser iniciado: <categoria>) também dáNotInspected, com os detalhes (caminhos inclusive) só na notificaçãoOnIsolationEvent. Não há fallback silencioso para o parsing em processo. - Windows: Job Object por worker —
KILL_ON_JOB_CLOSE, um único processo (sem filhos), teto de memória comprometida, sem diálogo de crash. - Só melhor esforço: em Linux/macOS o limite de memória é o teto do heap do GC (
DOTNET_GCHeapHardLimit) — a memória nativa não é limitada e não há limite de CPU além do timeout; um vigia do processo pai e o EOF do stdin encerram workers órfãos. Use limites do contêiner para garantias rígidas. Dentro dos 20 s um arquivo hostil ainda pode consumir a CPU de um worker; uma enxurrada de uploads hostis reduz a vazão (mantenha o rate limit), mas não derruba o hospedeiro. - Não é sandbox: o worker roda com a identidade e o acesso a arquivos/rede do hospedeiro. A isolação protege a disponibilidade; não contém execução de código no worker. A guarda pré-Open da 0.3.0 segue como primeira camada dentro do worker, e as lacunas residuais dela deixam de derrubar o hospedeiro.
- Implantação: o hospedeiro precisa poder iniciar processos filhos e ter o runtime .NET 10. Azure App Service:
plano dedicado (B1+) ou contêiner Linux; Free/Shared restringem processos filhos.
Isolation = InProcessé inseguro para PDF hostil.
These are conscious gaps in the heuristic layer; ClamAV (when enabled) and the caller responsibilities above are the complementary layers.
- Non-Flate stream filters. PDF scanning covers the structural regions
through PdfPig (xref/trailer, indirect references and object streams), plus stream bodies that are literal
(no
/Filter) or FlateDecode (always decompressed and scanned inflated — compressed bytes are never judged raw, since random compressed data produces false positives). Bodies behind other filters (ASCIIHex, ASCII85, LZW, or chained filters), inside an encrypted PDF, in a structurally invalid file, behind an unsupported predictor, or past per-stream/aggregate entry, expansion, attachment-count or recursion limits are not silently skipped: the scan returnsNotInspected(neverClean) so the caller fails closed. Actually reading those bodies would require CDR or a sandbox. Embedded files are identified throughFileSpecassociations (EmbeddedFiles,AF, andFSinside/Subtype /FileAttachmentannotations) and direct or indirect/EFrelations; the optional/Type /EmbeddedFilemarker is not trusted as the sole source. Unrelated/EFor/FSextension keys are ignored. Invalid name-tree values (including/Limitson the root node) and broken/cyclic/non-stream associated targets returnNotInspected. Literal and hexadecimal name keys and child/Limitsare compared by decoded bytes. Cancellation propagates through parsing and decompression. - The "full inspection or
NotInspected" guarantee covers PDF and OOXML only. For legacy OLE2 (.doc/.xls), images, CSV and plain text the engine is a best-effort byte-scanning heuristic and never reports incomplete inspection:Cleanthere means "no marker matched", not "fully parsed". An obfuscated or compressed OLE2 VBA macro can evade the raw scan and still come backClean— if you accept OLE2 uploads, plug in an antivirus (IVirusScanner) or block those extensions viaAllowedExtensions.
São lacunas conscientes da camada heurística; o ClamAV (quando habilitado) e as responsabilidades de quem chama (acima) são as camadas complementares.
- Filtros de stream não-Flate. O scan de PDF cobre as regiões estruturais
via PdfPig (xref/trailer, referências indiretas e object streams) e os corpos de stream
literais (sem
/Filter) ou FlateDecode (sempre descomprimidos e varridos inflados — bytes comprimidos nunca são julgados crus, porque dados comprimidos aleatórios geram falso positivo). Corpos atrás de outros filtros (ASCIIHex, ASCII85, LZW ou filtros encadeados), com predictor não suportado, dentro de PDF criptografado, em estrutura inválida ou além dos limites por stream/agregados de entradas, expansão, anexos ou profundidade não são pulados em silêncio: o scan devolveNotInspected(nuncaClean) e o chamador falha fechado. LER esses corpos de fato exigiria CDR ou sandbox. Arquivos embutidos são identificados por associaçõesFileSpec(EmbeddedFiles,AFeFSdentro de annotations/Subtype /FileAttachment) e relações/EFdiretas ou indiretas; o marcador opcional/Type /EmbeddedFilenão é a única fonte. Chaves de extensão/EFou/FSsem esse contexto são ignoradas. Valores inválidos na name tree (inclusive/Limitsno nó raiz) e alvos associados quebrados, cíclicos ou que não sejam stream devolvemNotInspected. Chaves e/Limitsliterais ou hexadecimais são comparados pelos bytes decodificados. Cancelamento é propagado por parsing e descompressão. - A garantia "inspeção integral ou
NotInspected" cobre só PDF e OOXML. Para OLE2 legado (.doc/.xls), imagens, CSV e texto puro o motor é heurística best-effort de varredura de bytes e nunca reporta inspeção incompleta:Cleannesses formatos significa "nenhum marcador casou", não "parseado por completo". Uma macro VBA OLE2 ofuscada ou comprimida pode evadir a varredura crua e ainda voltarClean— se você aceita uploads OLE2, plugue um antivírus (IVirusScanner) ou bloqueie essas extensões viaAllowedExtensions.
If you find a security issue in FileScan itself (e.g. a way to bypass a check, a crash/DoS, or a false-negative class), please open a private report via GitHub Security Advisories, or open an issue without including a working malicious payload.
Please do not attach real malware or real user documents to public issues.
The _testfiles/ directory contains synthetic proof-of-concept samples (benign
demonstrators such as app.alert, calc, EICAR, and <?php echo>). Do not add real
malware or real user data to this repository.