Zero-dependency Node.js helper for upload type checks: extension, size, magic numbers, plus a small PDF and SVG content policy.
Aligned with the file-type checking part of OWASP Unrestricted File Upload and CWE-434. Not an antivirus. Not a complete upload-security stack.
Current release: 2.0.0. See CHANGELOG.md.
npm install secure-file-validatorRequires Node.js 14.16+ (ESM).
import { validateFile } from "secure-file-validator";
const result = await validateFile("uploads/photo.jpg");
if (result.ok) {
// result.code === "OK"
} else {
// result.code === "INVALID_SIGNATURE" | "PDF_JAVASCRIPT" | ...
console.error(result.code, result.message);
}result.status still exists as a deprecated alias of result.ok.
import { validateFile, validateBytes } from "secure-file-validator";
const result = await validateFile(req.file.buffer, {
filename: req.file.originalname, // or extension: ".png"
});
if (!result.ok) {
throw new Error(result.code);
}
const sync = validateBytes(req.file.buffer, { extension: ".png" });Buffer / Uint8Array input must include filename or extension. Size is checked with byteLength.
| Extension | Magic check |
|---|---|
.jpg / .jpeg |
FF D8 FF (any marker after SOI) |
.png |
89 50 4E 47 |
.gif |
47 49 46 38 |
.pdf |
%PDF + %%EOF, then token policy |
.svg |
<?xml or <svg, then SVG policy |
Default size cap: 5MB (options.maxSizeInBytes).
The checker looks for PDF name tokens, including hex-escaped names (/J#53 → /JS) and names inside inflated Flate streams. The letters JS in ordinary text or XMP are not a hit.
| Token | Default | code |
|---|---|---|
/Metadata |
allow | PDF_METADATA |
/Annots |
allow | PDF_ANNOTS |
/OpenAction |
allow | PDF_OPEN_ACTION |
/JS, /JavaScript |
deny | PDF_JAVASCRIPT |
/Launch |
deny | PDF_LAUNCH |
/EmbeddedFile |
deny | PDF_EMBEDDED_FILE |
/XFA |
deny | PDF_XFA |
/RichMedia |
deny | PDF_RICH_MEDIA |
const strict = await validateFile(pdfPath, {
pdf: { allowOpenAction: false },
});
// Dangerous: turns the script check off
const trusted = await validateFile(pdfPath, {
pdf: { allowJavaScript: true },
});pdfWhitelist still works as a deprecated alias (JS and JavaScript both set allowJavaScript). Do not allow JavaScript only to silence metadata — metadata already passes. See #1.
Always denied: <script>, javascript:, event handlers (onload= …), <!ENTITY.
Denied by default, overridable with options.svg:
| Rule | Default | code |
|---|---|---|
foreignObject |
deny | SVG_FOREIGN_OBJECT |
data: URI |
deny | SVG_DATA_URI |
external href |
deny | SVG_EXTERNAL_HREF |
Allowed: DOCTYPE without ENTITY, fragment href="#id".
await validateFile(svgPath, {
svg: { allowDataUri: true },
});{
ok: false,
status: false, // deprecated alias of ok
code: "PDF_JAVASCRIPT",
message: "Suspicious PDF name token detected: /JavaScript",
details: { token: "JavaScript" }
}code |
Meaning |
|---|---|
OK |
Passed |
UNSUPPORTED_TYPE |
Extension not in the allow list |
INVALID_EXTENSION |
Bad or missing extension (buffers need filename / extension) |
TOO_LARGE |
Over maxSizeInBytes |
INVALID_SIGNATURE |
Magic number does not match the extension |
IO_ERROR |
Missing file or unreadable input |
PDF_* / SVG_* |
See the policy tables |
Switch on code. Do not parse message.
validateFile(input, options?): Promise<ValidationResult>
validateFileContent(input, options?): Promise<ValidationResult>
validateBytes(bytes, options?): ValidationResultinput is a filesystem path, Buffer, or Uint8Array.
| Option | Type | Default |
|---|---|---|
maxSizeInBytes |
number | 5 * 1024 * 1024 |
filename |
string | required for buffers if extension is omitted |
extension |
string | .png or png |
pdf |
PdfPolicy |
see table |
svg |
SvgPolicy |
see table |
pdfWhitelist |
string[] | deprecated alias |
| 1.x | 2.x |
|---|---|
result.status |
result.ok (status still works) |
result.message only |
also result.code |
| path string only | path, Buffer, or Uint8Array |
JPEG only FF D8 FF E0/E1 |
any FF D8 FF |
PDF substring /JS/, /Metadata/ |
name tokens; metadata allowed by default |
pdfWhitelist: ['JS'] to dodge false positives |
not needed for metadata; use pdf.allowJavaScript only if you mean it |
Does: type vs magic-number check, size cap, PDF name-token policy, SVG active-content policy.
Does not: antivirus, Office/ZIP unpacking, zip-bomb detection, filename sanitization, storage layout, Content-Type on the way out, auth, or rate limits.
Suggested pipeline:
- Validate bytes (
validateFile(buffer, { filename })). - Store under a generated name, not the client filename.
- Keep files outside the webroot.
- Serve a
Content-Typeyou chose, not one taken from the client.
Legitimate PDFs used to fail with /JS/ or /Metadata/.
That was a substring false positive (#1). 2.0.0 matches name tokens only. Metadata / Annots / OpenAction pass by default.
Is a passing result enough to call an upload safe?
No. This reduces wrong-type and obvious active-content mistakes. Pair it with generated names, isolated storage, and a tight serving policy.
MIT — see LICENSE.