pompelmi 0.11.0-dev.13 → 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.
- package/README.md +36 -37
- 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** —
|
|
45
|
-
- **Real
|
|
46
|
-
- **
|
|
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 +
|
|
76
|
+
**At a glance (policy + scanners)**
|
|
76
77
|
|
|
77
78
|
```ts
|
|
78
|
-
//
|
|
79
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
350
|
+
Use the examples above, then send a **minimal PDF** that contains risky tokens (this triggers the built‑in heuristics).
|
|
349
351
|
|
|
350
|
-
**1)
|
|
352
|
+
**1) Create a tiny PDF with risky actions**
|
|
351
353
|
|
|
352
354
|
Linux:
|
|
353
|
-
|
|
354
355
|
```bash
|
|
355
|
-
|
|
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
|
-
|
|
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=@
|
|
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
|
|
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",
|