pompelmi 0.11.0-dev.12 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +36 -37
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -41,13 +41,14 @@
41
41
 
42
42
  ## Highlights
43
43
 
44
- - **Block risky uploads early** — mark files as <em>clean</em>, <em>suspicious</em>, or <em>malicious</em> and stop them at the edge.
45
- - **Real checks** — extension allow‑list, MIME sniffing (magic bytes), per‑file size caps, and **deep ZIP** traversal with anti‑bomb limits.
46
- - **Pluggable scanner** — bring your own engine (e.g. YARA) via a minimal `{ scan(bytes) }` contract.
44
+ - **Block risky uploads early** — classify uploads as _clean_, _suspicious_, or _malicious_ and stop them at the edge.
45
+ - **Real guards** — extension allow‑list, server‑side MIME sniff (magic bytes), per‑file size caps, and **deep ZIP** traversal with anti‑bomb limits.
46
+ - **Built‑in scanners** — drop‑in **CommonHeuristicsScanner** (PDF risky actions, Office macros, PE header) and **Zip‑bomb Guard**; add your own or YARA via a tiny `{ scan(bytes) }` contract.
47
+ - **Compose scanning** — run multiple scanners in parallel or sequentially with timeouts and short‑circuiting via `composeScanners()`.
47
48
  - **Zero cloud** — scans run in‑process. Keep bytes private.
48
49
  - **DX first** — TypeScript types, ESM/CJS builds, tiny API, adapters for popular web frameworks.
49
50
 
50
- > Keywords: file upload security, malware scanning, YARA, Node.js, Express, Koa, Next.js, ZIP scanning
51
+ > Keywords: file upload security, malware scanning, YARA, Node.js, Express, Koa, Next.js, ZIP scanning, ZIP bomb, PDF JavaScript, Office macros
51
52
 
52
53
  ---
53
54
 
@@ -72,28 +73,30 @@ yarn add pompelmi
72
73
 
73
74
  ## Quick‑start
74
75
 
75
- **At a glance (policy + scanner)**
76
+ **At a glance (policy + scanners)**
76
77
 
77
78
  ```ts
78
- // Create a tiny scanner (matches EICAR test string)
79
- const SimpleEicarScanner = {
80
- async scan(bytes: Uint8Array) {
81
- const text = Buffer.from(bytes).toString('utf8');
82
- return text.includes('EICAR-STANDARD-ANTIVIRUS-TEST-FILE') ? [{ rule: 'eicar_test' }] : [];
83
- }
84
- };
79
+ // Compose built‑in scanners (no EICAR). Optionally add your own/YARA.
80
+ import { CommonHeuristicsScanner, createZipBombGuard, composeScanners } from 'pompelmi';
85
81
 
86
- // Example policy used by all adapters
87
- const policy = {
88
- scanner: SimpleEicarScanner,
89
- includeExtensions: ['txt','png','jpg','jpeg','pdf','zip'],
90
- allowedMimeTypes: ['text/plain','image/png','image/jpeg','application/pdf','application/zip'],
82
+ export const policy = {
83
+ includeExtensions: ['zip','png','jpg','jpeg','pdf'],
84
+ allowedMimeTypes: ['application/zip','image/png','image/jpeg','application/pdf','text/plain'],
91
85
  maxFileSizeBytes: 20 * 1024 * 1024,
92
86
  timeoutMs: 5000,
93
87
  concurrency: 4,
94
88
  failClosed: true,
95
89
  onScanEvent: (ev: unknown) => console.log('[scan]', ev)
96
90
  };
91
+
92
+ export const scanner = composeScanners(
93
+ [
94
+ ['zipGuard', createZipBombGuard({ maxEntries: 512, maxTotalUncompressedBytes: 100 * 1024 * 1024, maxCompressionRatio: 12 })],
95
+ ['heuristics', CommonHeuristicsScanner],
96
+ // ['yara', YourYaraScanner],
97
+ ],
98
+ { parallel: false, stopOn: 'suspicious', timeoutMsPerScanner: 1500, tagSourceName: true }
99
+ );
97
100
  ```
98
101
 
99
102
  ### Express
@@ -102,11 +105,12 @@ const policy = {
102
105
  import express from 'express';
103
106
  import multer from 'multer';
104
107
  import { createUploadGuard } from '@pompelmi/express-middleware';
108
+ import { policy, scanner } from './security'; // the snippet above
105
109
 
106
110
  const app = express();
107
- const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 } });
111
+ const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: policy.maxFileSizeBytes } });
108
112
 
109
- app.post('/upload', upload.any(), createUploadGuard(policy), (req, res) => {
113
+ app.post('/upload', upload.any(), createUploadGuard({ ...policy, scanner }), (req, res) => {
110
114
  res.json({ ok: true, scan: (req as any).pompelmi ?? null });
111
115
  });
112
116
 
@@ -120,12 +124,13 @@ import Koa from 'koa';
120
124
  import Router from '@koa/router';
121
125
  import multer from '@koa/multer';
122
126
  import { createKoaUploadGuard } from '@pompelmi/koa-middleware';
127
+ import { policy, scanner } from './security';
123
128
 
124
129
  const app = new Koa();
125
130
  const router = new Router();
126
- const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 } });
131
+ const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: policy.maxFileSizeBytes } });
127
132
 
128
- router.post('/upload', upload.any(), createKoaUploadGuard(policy), (ctx) => {
133
+ router.post('/upload', upload.any(), createKoaUploadGuard({ ...policy, scanner }), (ctx) => {
129
134
  ctx.body = { ok: true, scan: (ctx as any).pompelmi ?? null };
130
135
  });
131
136
 
@@ -138,16 +143,12 @@ app.listen(3003, () => console.log('http://localhost:3003'));
138
143
  ```ts
139
144
  // app/api/upload/route.ts
140
145
  import { createNextUploadHandler } from '@pompelmi/next-upload';
146
+ import { policy, scanner } from '@/lib/security';
141
147
 
142
148
  export const runtime = 'nodejs';
143
149
  export const dynamic = 'force-dynamic';
144
150
 
145
- const SimpleEicarScanner = policy.scanner; // reuse the same scanner
146
-
147
- export const POST = createNextUploadHandler({
148
- ...policy,
149
- scanner: SimpleEicarScanner
150
- });
151
+ export const POST = createNextUploadHandler({ ...policy, scanner });
151
152
  ```
152
153
 
153
154
  ---
@@ -207,7 +208,7 @@ Use the adapter that matches your web framework. All adapters share the same pol
207
208
  | Express | `@pompelmi/express-middleware` | alpha |
208
209
  | Koa | `@pompelmi/koa-middleware` | alpha |
209
210
  | Next.js (App Router) | `@pompelmi/next-upload` | alpha |
210
- | Fastify | fastify plugin planned |
211
+ | Fastify | `@pompelmi/fastify-plugin` | alpha |
211
212
  | NestJS | nestjs — planned |
212
213
  | Remix | remix — planned |
213
214
  | hapi | hapi plugin — planned |
@@ -335,6 +336,7 @@ failClosed: true,
335
336
  - [ ] **Restrict extensions & MIME** to what your app truly needs.
336
337
  - [ ] **Set `failClosed: true` in production** to block on timeouts/errors.
337
338
  - [ ] **Handle ZIPs carefully** (enable deep ZIP, keep nesting low, cap entry sizes).
339
+ - [ ] **Compose scanners** with `composeScanners()` and enable `stopOn` to fail fast on early detections.
338
340
  - [ ] **Log scan events** (`onScanEvent`) and monitor for spikes.
339
341
  - [ ] **Run scans in a separate process/container** for defense‑in‑depth when possible.
340
342
  - [ ] **Sanitize file names and paths** if you persist uploads.
@@ -343,30 +345,27 @@ failClosed: true,
343
345
 
344
346
  ---
345
347
 
346
- ## Quick test (EICAR)
348
+ ## Quick test (no EICAR)
347
349
 
348
- Use the examples above, then send the standard **EICAR** test file to verify endto‑end blocking.
350
+ Use the examples above, then send a **minimal PDF** that contains risky tokens (this triggers the builtin heuristics).
349
351
 
350
- **1) Generate the EICAR file (safe test string)**
352
+ **1) Create a tiny PDF with risky actions**
351
353
 
352
354
  Linux:
353
-
354
355
  ```bash
355
- echo 'WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNULUZJTEUhJEgrSCo=' | base64 -d > eicar.txt
356
+ printf '%%PDF-1.7\n1 0 obj\n<< /OpenAction 1 0 R /AA << /JavaScript (alert(1)) >> >>\nendobj\n%%EOF\n' > risky.pdf
356
357
  ```
357
358
 
358
359
  macOS:
359
-
360
360
  ```bash
361
- echo 'WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNULUZJTEUhJEgrSCo=' | base64 -D > eicar.txt
361
+ printf '%%PDF-1.7\n1 0 obj\n<< /OpenAction 1 0 R /AA << /JavaScript (alert(1)) >> >>\nendobj\n%%EOF\n' > risky.pdf
362
362
  ```
363
363
 
364
364
  **2) Send it to your endpoint**
365
365
 
366
366
  Express (default from the Quick‑start):
367
-
368
367
  ```bash
369
- curl -F "file=@eicar.txt;type=text/plain" http://localhost:3000/upload -i
368
+ curl -F "file=@risky.pdf;type=application/pdf" http://localhost:3000/upload -i
370
369
  ```
371
370
 
372
371
  You should see an HTTP **422 Unprocessable Entity** (blocked by policy). Clean files return **200 OK**. Pre‑filter failures (size/ext/MIME) should return a **4xx**. Adapt these conventions to your app as needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pompelmi",
3
- "version": "0.11.0-dev.12",
3
+ "version": "0.11.0",
4
4
  "description": "RFI-safe file uploads for Node.js — Express/Koa/Next.js middleware with deep ZIP inspection, MIME/size checks, and optional YARA scanning.",
5
5
  "main": "dist/pompelmi.cjs.js",
6
6
  "module": "dist/pompelmi.esm.js",