pompelmi 0.35.3 → 0.35.5
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 +125 -85
- package/dist/pompelmi.browser.cjs +99 -43
- package/dist/pompelmi.browser.cjs.map +1 -1
- package/dist/pompelmi.browser.esm.js +99 -43
- package/dist/pompelmi.browser.esm.js.map +1 -1
- package/dist/pompelmi.cjs +99 -43
- package/dist/pompelmi.cjs.map +1 -1
- package/dist/pompelmi.esm.js +99 -43
- package/dist/pompelmi.esm.js.map +1 -1
- package/dist/pompelmi.react.cjs +99 -43
- package/dist/pompelmi.react.cjs.map +1 -1
- package/dist/pompelmi.react.esm.js +99 -43
- package/dist/pompelmi.react.esm.js.map +1 -1
- package/dist/types/src/scanners/zip-bomb-guard.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,54 +1,100 @@
|
|
|
1
|
+
<!-- ════════════════════════════════════════════════════════════════════
|
|
2
|
+
PIVOT NOTICE — read before using this package
|
|
3
|
+
════════════════════════════════════════════════════════════════════ -->
|
|
4
|
+
> [!CAUTION]
|
|
5
|
+
> **v0.x is an experimental prototype. Do NOT use it in production.**
|
|
6
|
+
>
|
|
7
|
+
> Pompelmi v0.x has **known Event Loop blocking issues** and makes overreaching
|
|
8
|
+
> "scanner" claims that it cannot keep. This version is in **soft maintenance
|
|
9
|
+
> only** — no new features, security patches only.
|
|
10
|
+
>
|
|
11
|
+
> ---
|
|
12
|
+
>
|
|
13
|
+
> **Pompelmi is pivoting for v1.0.**
|
|
14
|
+
>
|
|
15
|
+
> The new identity is a **dead-simple, one-line utility wrapper** for Node.js
|
|
16
|
+
> file uploads — not an "ultimate malware scanner". v1.0 will bundle standard,
|
|
17
|
+
> well-understood checks (size limits, magic bytes, basic heuristics) into a
|
|
18
|
+
> single convenient call that returns a traffic-light verdict:
|
|
19
|
+
>
|
|
20
|
+
> ```js
|
|
21
|
+
> const result = await pompelmi.scan(file);
|
|
22
|
+
> // returns 'green', 'suspicious', or 'malicious'
|
|
23
|
+
> ```
|
|
24
|
+
>
|
|
25
|
+
> No magic promises. No impossible guarantees. Just saved developer time.
|
|
26
|
+
>
|
|
27
|
+
> Follow the pivot → [GitHub Issues](https://github.com/pompelmi/pompelmi/issues) · [Discussions](https://github.com/pompelmi/pompelmi/discussions)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
1
31
|
<div align="center">
|
|
2
|
-
<img src="./assets/logo.svg" alt="Pompelmi logo" width="
|
|
32
|
+
<img src="./assets/logo.svg" alt="Pompelmi logo" width="120" />
|
|
3
33
|
|
|
4
34
|
<h1>Pompelmi</h1>
|
|
5
35
|
|
|
6
|
-
<p><strong>
|
|
7
|
-
|
|
8
|
-
<p>Inspect untrusted uploads before storage.</p>
|
|
36
|
+
<p><strong>Secure file uploads in Node.js before storage.</strong></p>
|
|
9
37
|
|
|
10
38
|
<p>
|
|
11
|
-
|
|
12
|
-
|
|
39
|
+
Open-source route-level upload security for Node.js teams that need to
|
|
40
|
+
inspect untrusted files before disk, object storage, previews, or
|
|
41
|
+
downstream parsers.
|
|
13
42
|
</p>
|
|
14
43
|
|
|
15
44
|
<p><code>clean</code> · <code>suspicious</code> · <code>malicious</code></p>
|
|
16
45
|
|
|
17
46
|
<p>
|
|
18
|
-
|
|
47
|
+
MIME spoofing · risky archives · document and binary signals · optional
|
|
48
|
+
YARA
|
|
19
49
|
</p>
|
|
20
50
|
|
|
21
|
-
<p
|
|
51
|
+
<p>
|
|
52
|
+
<sub>Express · Next.js · NestJS · Fastify · Koa · Nuxt/Nitro · S3 quarantine flows · CI/CD</sub>
|
|
53
|
+
</p>
|
|
22
54
|
|
|
23
55
|
<p>
|
|
24
56
|
<a href="https://www.npmjs.com/package/pompelmi"><img alt="npm version" src="https://img.shields.io/npm/v/pompelmi" /></a>
|
|
57
|
+
<a href="https://www.npmjs.com/package/pompelmi"><img alt="npm total downloads" src="https://img.shields.io/npm/dt/pompelmi" /></a>
|
|
25
58
|
<a href="https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/pompelmi/pompelmi/ci.yml?label=ci" /></a>
|
|
26
59
|
<a href="https://codecov.io/gh/pompelmi/pompelmi"><img alt="codecov" src="https://codecov.io/gh/pompelmi/pompelmi/graph/badge.svg" /></a>
|
|
27
60
|
<a href="https://github.com/pompelmi/pompelmi/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/pompelmi/pompelmi?style=social" /></a>
|
|
28
|
-
<a href="https://
|
|
29
|
-
<a href="https://
|
|
61
|
+
<a href="https://github.com/pompelmi/pompelmi/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green.svg" /></a>
|
|
62
|
+
<a href="https://nodejs.org"><img alt="Node.js 18+" src="https://img.shields.io/badge/node-%3E%3D18-brightgreen" /></a>
|
|
30
63
|
</p>
|
|
31
64
|
|
|
65
|
+
> If Pompelmi fits how you think about upload security at the route
|
|
66
|
+
> level, starring the repo helps other Node.js teams find it.
|
|
67
|
+
> [★ Star on GitHub](https://github.com/pompelmi/pompelmi/stargazers)
|
|
68
|
+
|
|
32
69
|
<p>
|
|
33
|
-
<a href="https://pompelmi.
|
|
70
|
+
<a href="https://pompelmi.app/"><strong>Docs</strong></a>
|
|
71
|
+
·
|
|
72
|
+
<a href="https://pompelmi.app/getting-started/"><strong>Getting started</strong></a>
|
|
34
73
|
·
|
|
35
|
-
<a href="https://pompelmi.
|
|
74
|
+
<a href="https://pompelmi.app/#browser-preview"><strong>Browser preview</strong></a>
|
|
36
75
|
·
|
|
37
76
|
<a href="./examples/demo"><strong>Express demo</strong></a>
|
|
38
77
|
·
|
|
39
78
|
<a href="./examples/README.md"><strong>Examples</strong></a>
|
|
40
79
|
</p>
|
|
80
|
+
|
|
81
|
+
<p><sub>Node.js 18+ · MIT</sub></p>
|
|
41
82
|
</div>
|
|
42
83
|
|
|
43
84
|
<p align="center">
|
|
44
|
-
Mentioned by
|
|
85
|
+
<strong>Mentioned by</strong>
|
|
86
|
+
<a href="https://nodeweekly.com/issues/594">Node Weekly</a>,
|
|
45
87
|
<a href="https://stackoverflow.blog/2026/02/23/defense-against-uploads-oss-file-scanner-pompelmi/">Stack Overflow</a>,
|
|
46
88
|
<a href="https://www.helpnetsecurity.com/2026/02/02/pompelmi-open-source-secure-file-upload-scanning-node-js/">Help Net Security</a>,
|
|
47
89
|
<a href="https://github.com/sorrycc/awesome-javascript">Awesome JavaScript</a>,
|
|
90
|
+
<a href="https://github.com/dzharii/awesome-typescript">Awesome TypeScript</a>,
|
|
91
|
+
<a href="https://bytes.dev/archives/429">Bytes</a>,
|
|
48
92
|
and
|
|
49
|
-
<a href="https://
|
|
93
|
+
<a href="https://www.detectionengineering.net/p/det-eng-weekly-124">Detection Engineering Weekly</a>
|
|
50
94
|
</p>
|
|
51
95
|
|
|
96
|
+
> Upload endpoints are part of your attack surface. Pompelmi helps Node.js teams scan files before storage and make the decision while the route still has context: accept, quarantine, or reject.
|
|
97
|
+
|
|
52
98
|
## Quick Start
|
|
53
99
|
|
|
54
100
|
Install the core package:
|
|
@@ -57,7 +103,7 @@ Install the core package:
|
|
|
57
103
|
npm install pompelmi
|
|
58
104
|
```
|
|
59
105
|
|
|
60
|
-
|
|
106
|
+
This is the core pattern: inspect bytes, get a verdict, and only store clean files.
|
|
61
107
|
|
|
62
108
|
```ts
|
|
63
109
|
import { scanBytes, STRICT_PUBLIC_UPLOAD } from 'pompelmi';
|
|
@@ -80,104 +126,98 @@ if (report.verdict !== 'clean') {
|
|
|
80
126
|
return res.status(200).json({ verdict: report.verdict });
|
|
81
127
|
```
|
|
82
128
|
|
|
83
|
-
Start with [Getting started](https://pompelmi.
|
|
129
|
+
Start with [Getting started](https://pompelmi.app/getting-started/) for a local scan in under a minute, open the [browser preview](https://pompelmi.app/#browser-preview) to inspect the verdict flow without sending files anywhere, or run the minimal [Express demo](./examples/demo).
|
|
84
130
|
|
|
85
|
-
|
|
131
|
+
## Why teams use Pompelmi
|
|
86
132
|
|
|
87
|
-
|
|
133
|
+
- File upload endpoints are not just form validation. Files can become risky after storage, extraction, rendering, or downstream parsing.
|
|
134
|
+
- Pompelmi keeps the first trust decision inside the application path, where the route still knows the file class, trust level, storage path, and failure mode.
|
|
135
|
+
- It gives Node.js teams a practical way to build secure file uploads with route-level decisions instead of bolting checks on after persistence.
|
|
88
136
|
|
|
89
|
-
|
|
137
|
+
File upload vulnerabilities are the root cause of real CVEs across web frameworks. Application-layer inspection is the earliest point where the route still has full policy context — file class, trust level, storage path, and failure mode. Waiting until after storage removes most of that context and limits what the application can decide.
|
|
90
138
|
|
|
91
|
-
|
|
139
|
+
## What it checks
|
|
92
140
|
|
|
93
|
-
|
|
141
|
+
- MIME sniffing, magic-byte validation, and extension mismatch detection
|
|
142
|
+
- Risky archives such as ZIP bombs, traversal attempts, deep nesting, and entry-count abuse
|
|
143
|
+
- Risky document and binary signals such as PDF actions, Office macro hints, PE headers, and polyglot files
|
|
144
|
+
- Optional YARA-based matches when you want malware scanning uploads as part of the flow
|
|
145
|
+
- Verdicts and reasons you can use for fail-closed routes, quarantine flows, and auditability
|
|
94
146
|
|
|
95
|
-
|
|
96
|
-
- risky archive structures such as traversal, deep nesting, entry-count abuse, and ZIP bomb-style expansion
|
|
97
|
-
- suspicious document and binary signals such as risky PDF actions, Office macro hints, PE headers, and polyglot files
|
|
98
|
-
- optional YARA or other scanner matches
|
|
99
|
-
- route-level verdicts that support reject, quarantine, or promote workflows
|
|
147
|
+
## Where it fits in the upload pipeline
|
|
100
148
|
|
|
101
|
-
|
|
149
|
+
1. Receive the upload into memory or an isolated staging or quarantine area.
|
|
150
|
+
2. Scan the bytes with a route policy.
|
|
151
|
+
3. Act on the verdict: `clean`, `suspicious`, or `malicious`.
|
|
152
|
+
4. Persist, quarantine, or reject based on the route's rules.
|
|
102
153
|
|
|
103
|
-
-
|
|
104
|
-
- memory-backed multipart routes in Express, Next.js, NestJS, Fastify, and Koa
|
|
105
|
-
- quarantine and promotion workflows for S3 or other object storage
|
|
106
|
-
- document, image, and archive routes that need different policies
|
|
107
|
-
- CI/CD or internal artifact scanning before promotion
|
|
154
|
+
That inspect-first, store-later shape is where Pompelmi is strongest.
|
|
108
155
|
|
|
109
|
-
##
|
|
156
|
+
## What it is and isn't
|
|
110
157
|
|
|
111
158
|
| Approach | Useful for | What it misses |
|
|
112
159
|
| --- | --- | --- |
|
|
113
|
-
| Browser MIME and extension checks | Fast client-side hints and UX feedback |
|
|
114
|
-
|
|
|
115
|
-
| Antivirus
|
|
116
|
-
| Pompelmi at the upload route |
|
|
117
|
-
|
|
118
|
-
## Integrations
|
|
160
|
+
| Browser MIME and extension checks | Fast client-side hints and UX feedback | Client MIME and filenames are easy to spoof |
|
|
161
|
+
| File-type or magic-byte validation only | Confirming a file looks like the claimed type | Archive abuse, risky internal structure, and route policy decisions |
|
|
162
|
+
| Antivirus or YARA only | Known malicious matches and signature-style detection | Route context, spoofing checks, and before-storage handling |
|
|
163
|
+
| Pompelmi at the upload route | Node.js file upload security, scan files before storage, and verdict-driven workflow decisions | It is not a full antivirus replacement on its own |
|
|
119
164
|
|
|
120
|
-
|
|
121
|
-
- Next.js: [Docs](https://pompelmi.github.io/pompelmi/how-to/nextjs/) · [Example](./examples/next-app-router)
|
|
122
|
-
- NestJS: [Docs](https://pompelmi.github.io/pompelmi/how-to/nestjs/) · [Example app](./examples/nestjs-app)
|
|
123
|
-
- Fastify: [Docs](https://pompelmi.github.io/pompelmi/how-to/fastify/) · [Package](./packages/fastify-plugin)
|
|
124
|
-
- Koa: [Docs](https://pompelmi.github.io/pompelmi/how-to/koa/) · [Package](./packages/koa-middleware)
|
|
125
|
-
- Nuxt/Nitro: [Docs](https://pompelmi.github.io/pompelmi/how-to/nuxt-nitro/)
|
|
126
|
-
- S3 / object storage: [Tutorial](https://pompelmi.github.io/pompelmi/tutorials/secure-s3-presigned-uploads-with-malware-scanning/) · [Use case](https://pompelmi.github.io/pompelmi/use-cases/s3-presigned-upload-security/)
|
|
127
|
-
- CI/CD: [Use case](https://pompelmi.github.io/pompelmi/use-cases/cicd-artifact-scanning/) · [Blog](https://pompelmi.github.io/pompelmi/blog/cicd-scan-build-artifacts/)
|
|
165
|
+
<!-- search: file upload security Node.js, MIME spoofing protection, archive bomb defense -->
|
|
128
166
|
|
|
129
|
-
##
|
|
167
|
+
## Supported frameworks and workflows
|
|
130
168
|
|
|
131
|
-
|
|
169
|
+
| Stack or workflow | Links |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| Express | [Docs](https://pompelmi.app/how-to/express/) · [Minimal example](./examples/express-minimal) · [Demo](./examples/demo) |
|
|
172
|
+
| Next.js | [Docs](https://pompelmi.app/how-to/nextjs/) · [Example](./examples/next-app-router) · [Package](./packages/next-upload) |
|
|
173
|
+
| NestJS | [Docs](https://pompelmi.app/how-to/nestjs/) · [Package](./packages/nestjs) · [Example app](./examples/nestjs-app) |
|
|
174
|
+
| Fastify | [Docs](https://pompelmi.app/how-to/fastify/) · [Package](./packages/fastify-plugin) |
|
|
175
|
+
| Koa | [Docs](https://pompelmi.app/how-to/koa/) · [Package](./packages/koa-middleware) |
|
|
176
|
+
| Nuxt/Nitro | [Docs](https://pompelmi.app/how-to/nuxt-nitro/) · [Example](./examples/nuxt-nitro) |
|
|
177
|
+
| S3 / object storage | [Tutorial](https://pompelmi.app/tutorials/secure-s3-presigned-uploads-with-malware-scanning/) · [Use case](https://pompelmi.app/use-cases/object-storage-promotion-workflows/) |
|
|
178
|
+
| CI/CD | [Use case](https://pompelmi.app/use-cases/cicd-artifact-scanning/) · [Blog](https://pompelmi.app/blog/cicd-scan-build-artifacts/) |
|
|
132
179
|
|
|
133
|
-
|
|
134
|
-
- [Demo](./examples/demo) for a tiny Express upload gate that returns `clean`, `suspicious`, or `malicious` before storage
|
|
135
|
-
- [Examples index](./examples/README.md) for framework-specific and production-oriented examples
|
|
180
|
+
## Demo, preview, and examples
|
|
136
181
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
- [Docs home](https://pompelmi.github.io/pompelmi/)
|
|
140
|
-
- [Getting started](https://pompelmi.github.io/pompelmi/getting-started/)
|
|
141
|
-
- [Use cases](https://pompelmi.github.io/pompelmi/use-cases/)
|
|
142
|
-
- [Comparisons](https://pompelmi.github.io/pompelmi/comparisons/)
|
|
143
|
-
- [Tutorials](https://pompelmi.github.io/pompelmi/tutorials/)
|
|
144
|
-
- [Featured in](https://pompelmi.github.io/pompelmi/featured-in/)
|
|
145
|
-
- [Translations](https://pompelmi.github.io/pompelmi/translations/)
|
|
182
|
+

|
|
146
183
|
|
|
147
|
-
|
|
184
|
+
- [Browser preview](https://pompelmi.app/#browser-preview) for a fast local look at the verdict UX without uploading files anywhere
|
|
185
|
+
- [Express demo](./examples/demo) for a tiny upload gate that returns `clean`, `suspicious`, or `malicious` before storage
|
|
186
|
+
- [Examples index](./examples/README.md) for framework-specific and production-oriented patterns
|
|
187
|
+
- [Docs home](https://pompelmi.app/) for guides, comparisons, use cases, and tutorials
|
|
148
188
|
|
|
149
|
-
|
|
189
|
+
Listed in Awesome JavaScript and Awesome TypeScript, and featured by Node Weekly, Stack Overflow, Help Net Security, Bytes, and Detection Engineering Weekly.
|
|
150
190
|
|
|
151
191
|
<!-- MENTIONS:START -->
|
|
152
192
|
|
|
153
|
-
##
|
|
154
|
-
|
|
155
|
-
Full page: [pompelmi.github.io/pompelmi/featured-in](https://pompelmi.github.io/pompelmi/featured-in/)
|
|
156
|
-
|
|
157
|
-
*Last updated: March 20, 2026*
|
|
158
|
-
|
|
159
|
-
### Awesome Lists & Curated Collections
|
|
193
|
+
## Mentioned by
|
|
160
194
|
|
|
161
|
-
- [
|
|
162
|
-
- [
|
|
195
|
+
- [Node Weekly](https://nodeweekly.com/issues/594)
|
|
196
|
+
- [Defense against uploads: Q&A with OSS file scanner, pompelmi](https://stackoverflow.blog/2026/02/23/defense-against-uploads-oss-file-scanner-pompelmi/) — Stack Overflow
|
|
197
|
+
- [Pompelmi: Open-source secure file upload scanning for Node.js](https://www.helpnetsecurity.com/2026/02/02/pompelmi-open-source-secure-file-upload-scanning-node-js/) — Help Net Security
|
|
198
|
+
- [Awesome JavaScript](https://github.com/sorrycc/awesome-javascript)
|
|
199
|
+
- [Awesome TypeScript](https://github.com/dzharii/awesome-typescript)
|
|
200
|
+
- [Bytes #429](https://bytes.dev/archives/429) — bytes.dev (2025-10-03)
|
|
201
|
+
- [Det. Eng. Weekly #124](https://www.detectionengineering.net/p/det-eng-weekly-124) — detectionengineering.net (2025-08-13)
|
|
202
|
+
- [The Overflow #319](https://stackoverflow.blog/2026/03/04/the-overflow-319/) — stackoverflow.blog (2026-03-04)
|
|
203
|
+
- [Hottest OSS tools Feb 2026](https://www.helpnetsecurity.com/2026/02/26/hottest-oss-tools-feb-2026/) — helpnetsecurity.com (2026-02-26)
|
|
204
|
+
- [See all mentions](https://pompelmi.app/featured-in/)
|
|
163
205
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
- [The Overflow Issue 319: Dogfooding your SDLC](https://stackoverflow.blog/newsletter/issue-319-dogfooding-your-sdlc/) — Stack Overflow (2026-03-04)
|
|
167
|
-
- [Hottest cybersecurity open-source tools of the month: February 2026](https://www.helpnetsecurity.com/2026/02/26/hottest-cybersecurity-open-source-tools-of-the-month-february-2026/) — Help Net Security (2026-02-26)
|
|
168
|
-
- [Bytes #429](https://bytes.dev/archives/429) — Bytes (2025-10-03)
|
|
169
|
-
- [Node Weekly Issue 594](https://nodeweekly.com/issues/594) — Node Weekly (2025-09-30)
|
|
170
|
-
- [Det. Eng. Weekly Issue #124 - The DEFCON hangover is real](https://www.detectionengineering.net/p/det-eng-weekly-issue-124-the-defcon) — Detection Engineering (2025-08-13)
|
|
171
|
-
|
|
172
|
-
### Other Mentions
|
|
206
|
+
<!-- MENTIONS:END -->
|
|
173
207
|
|
|
174
|
-
|
|
175
|
-
- [Pompelmi: Open-source secure file upload scanning for Node.js](https://www.helpnetsecurity.com/2026/02/02/pompelmi-open-source-secure-file-upload-scanning-node-js/) — Help Net Security (2026-02-02)
|
|
208
|
+
## Docs
|
|
176
209
|
|
|
210
|
+
- [Getting started](https://pompelmi.app/getting-started/)
|
|
211
|
+
- [Use cases](https://pompelmi.app/use-cases/)
|
|
212
|
+
- [Comparisons](https://pompelmi.app/comparisons/)
|
|
213
|
+
- [Tutorials](https://pompelmi.app/tutorials/)
|
|
214
|
+
- [Browser preview](https://pompelmi.app/#browser-preview)
|
|
215
|
+
- [Featured in](https://pompelmi.app/featured-in/)
|
|
216
|
+
- [Translations](https://pompelmi.app/translations/)
|
|
177
217
|
|
|
178
|
-
|
|
218
|
+
## Commercial support
|
|
179
219
|
|
|
180
|
-
|
|
220
|
+
The MIT core remains the primary path. Teams that need private rollout help, architecture review, or policy tuning can use the existing [enterprise support path](https://pompelmi.app/enterprise/).
|
|
181
221
|
|
|
182
222
|
## Project
|
|
183
223
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var crypto = require('crypto');
|
|
4
|
+
var zlib = require('zlib');
|
|
4
5
|
|
|
5
6
|
const MB$1 = 1024 * 1024;
|
|
6
7
|
const DEFAULT_POLICY = {
|
|
@@ -1053,12 +1054,15 @@ async function scanFiles(files, opts = {}) {
|
|
|
1053
1054
|
return out;
|
|
1054
1055
|
}
|
|
1055
1056
|
|
|
1057
|
+
const ARCHIVE_BOMB_DETECTED = "ARCHIVE_BOMB_DETECTED";
|
|
1058
|
+
const SIG_LFH = 0x04034b50;
|
|
1056
1059
|
const SIG_CEN = 0x02014b50;
|
|
1057
1060
|
const DEFAULTS = {
|
|
1058
1061
|
maxEntries: 1000,
|
|
1059
1062
|
maxTotalUncompressedBytes: 500 * 1024 * 1024,
|
|
1063
|
+
maxPerEntryUncompressedBytes: 100 * 1024 * 1024,
|
|
1060
1064
|
maxEntryNameLength: 255,
|
|
1061
|
-
maxCompressionRatio:
|
|
1065
|
+
maxCompressionRatio: 100,
|
|
1062
1066
|
eocdSearchWindow: 70000,
|
|
1063
1067
|
};
|
|
1064
1068
|
function r16(buf, off) {
|
|
@@ -1068,7 +1072,6 @@ function r32(buf, off) {
|
|
|
1068
1072
|
return buf.readUInt32LE(off);
|
|
1069
1073
|
}
|
|
1070
1074
|
function isZipLike(buf) {
|
|
1071
|
-
// local file header at start is common
|
|
1072
1075
|
return (buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && buf[2] === 0x03 && buf[3] === 0x04);
|
|
1073
1076
|
}
|
|
1074
1077
|
function lastIndexOfEOCD(buf, window) {
|
|
@@ -1080,6 +1083,47 @@ function lastIndexOfEOCD(buf, window) {
|
|
|
1080
1083
|
function hasTraversal(name) {
|
|
1081
1084
|
return (name.includes("../") || name.includes("..\\") || name.startsWith("/") || /^[A-Za-z]:/.test(name));
|
|
1082
1085
|
}
|
|
1086
|
+
function makeBombError() {
|
|
1087
|
+
return Object.assign(new Error("Archive bomb detected: decompression limits exceeded"), {
|
|
1088
|
+
code: ARCHIVE_BOMB_DETECTED,
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Feeds `compressed` into a raw DEFLATE inflate stream and counts the actual
|
|
1093
|
+
* output bytes. Resolves with bombed=true and aborts early if any limit fires:
|
|
1094
|
+
* - decompressed bytes > maxPerEntry
|
|
1095
|
+
* - totalSoFar + decompressed > maxTotal
|
|
1096
|
+
* - decompressed / compressed > maxRatio (ratio measured on real bytes, not headers)
|
|
1097
|
+
*
|
|
1098
|
+
* Malformed DEFLATE is treated as safe (bombed=false, decompressed=0).
|
|
1099
|
+
*/
|
|
1100
|
+
function streamInflate(compressed, maxPerEntry, maxTotal, alreadySeen, maxRatio) {
|
|
1101
|
+
return new Promise((resolve) => {
|
|
1102
|
+
const inf = zlib.createInflateRaw();
|
|
1103
|
+
let out = 0;
|
|
1104
|
+
const compBytes = compressed.length;
|
|
1105
|
+
let done = false;
|
|
1106
|
+
const finish = (bombed) => {
|
|
1107
|
+
if (done)
|
|
1108
|
+
return;
|
|
1109
|
+
done = true;
|
|
1110
|
+
inf.destroy();
|
|
1111
|
+
resolve({ decompressed: out, bombed });
|
|
1112
|
+
};
|
|
1113
|
+
inf.on("data", (chunk) => {
|
|
1114
|
+
out += chunk.length;
|
|
1115
|
+
if (out > maxPerEntry ||
|
|
1116
|
+
alreadySeen + out > maxTotal ||
|
|
1117
|
+
(compBytes > 0 && out / compBytes > maxRatio)) {
|
|
1118
|
+
finish(true);
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
inf.on("end", () => finish(false));
|
|
1122
|
+
// Malformed DEFLATE stream → not a bomb, just corrupt
|
|
1123
|
+
inf.on("error", () => finish(false));
|
|
1124
|
+
inf.end(compressed);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1083
1127
|
function createZipBombGuard(opts = {}) {
|
|
1084
1128
|
const cfg = { ...DEFAULTS, ...opts };
|
|
1085
1129
|
return {
|
|
@@ -1088,43 +1132,36 @@ function createZipBombGuard(opts = {}) {
|
|
|
1088
1132
|
const matches = [];
|
|
1089
1133
|
if (!isZipLike(buf))
|
|
1090
1134
|
return matches;
|
|
1091
|
-
//
|
|
1135
|
+
// ── 1. Locate EOCD ──────────────────────────────────────────────────────
|
|
1092
1136
|
const eocdPos = lastIndexOfEOCD(buf, cfg.eocdSearchWindow);
|
|
1093
1137
|
if (eocdPos < 0 || eocdPos + 22 > buf.length) {
|
|
1094
|
-
// ZIP but no EOCD — malformed or polyglot → suspicious
|
|
1095
1138
|
matches.push({ rule: "zip_eocd_not_found", severity: "medium" });
|
|
1096
1139
|
return matches;
|
|
1097
1140
|
}
|
|
1098
1141
|
const totalEntries = r16(buf, eocdPos + 10);
|
|
1099
1142
|
const cdSize = r32(buf, eocdPos + 12);
|
|
1100
1143
|
const cdOffset = r32(buf, eocdPos + 16);
|
|
1101
|
-
// Bounds check
|
|
1102
1144
|
if (cdOffset + cdSize > buf.length) {
|
|
1103
1145
|
matches.push({ rule: "zip_cd_out_of_bounds", severity: "medium" });
|
|
1104
1146
|
return matches;
|
|
1105
1147
|
}
|
|
1106
|
-
|
|
1148
|
+
const lfhIndex = [];
|
|
1107
1149
|
let ptr = cdOffset;
|
|
1108
1150
|
let seen = 0;
|
|
1109
|
-
let sumComp = 0;
|
|
1110
|
-
let sumUnc = 0;
|
|
1111
1151
|
while (ptr + 46 <= cdOffset + cdSize && seen < totalEntries) {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
const compSize = r32(buf, ptr + 20);
|
|
1116
|
-
const uncSize = r32(buf, ptr + 24);
|
|
1152
|
+
if (r32(buf, ptr) !== SIG_CEN)
|
|
1153
|
+
break;
|
|
1154
|
+
const cdCompSize = r32(buf, ptr + 20);
|
|
1117
1155
|
const fnLen = r16(buf, ptr + 28);
|
|
1118
1156
|
const exLen = r16(buf, ptr + 30);
|
|
1119
1157
|
const cmLen = r16(buf, ptr + 32);
|
|
1120
|
-
const
|
|
1121
|
-
const nameEnd =
|
|
1158
|
+
const lfhOffset = r32(buf, ptr + 42);
|
|
1159
|
+
const nameEnd = ptr + 46 + fnLen;
|
|
1122
1160
|
if (nameEnd > buf.length)
|
|
1123
1161
|
break;
|
|
1124
|
-
const name = buf.toString("utf8",
|
|
1125
|
-
sumComp += compSize;
|
|
1126
|
-
sumUnc += uncSize;
|
|
1162
|
+
const name = buf.toString("utf8", ptr + 46, nameEnd);
|
|
1127
1163
|
seen++;
|
|
1164
|
+
lfhIndex.push({ lfhOffset, cdCompSize });
|
|
1128
1165
|
if (name.length > cfg.maxEntryNameLength) {
|
|
1129
1166
|
matches.push({
|
|
1130
1167
|
rule: "zip_entry_name_too_long",
|
|
@@ -1135,48 +1172,67 @@ function createZipBombGuard(opts = {}) {
|
|
|
1135
1172
|
if (hasTraversal(name)) {
|
|
1136
1173
|
matches.push({ rule: "zip_path_traversal_entry", severity: "medium", meta: { name } });
|
|
1137
1174
|
}
|
|
1138
|
-
// move to next entry
|
|
1139
1175
|
ptr = nameEnd + exLen + cmLen;
|
|
1140
1176
|
}
|
|
1141
1177
|
if (seen !== totalEntries) {
|
|
1142
|
-
// central dir truncated/odd, still report what we found
|
|
1143
1178
|
matches.push({
|
|
1144
1179
|
rule: "zip_cd_truncated",
|
|
1145
1180
|
severity: "medium",
|
|
1146
1181
|
meta: { seen, totalEntries },
|
|
1147
1182
|
});
|
|
1148
1183
|
}
|
|
1149
|
-
// Heuristics thresholds
|
|
1150
1184
|
if (seen > cfg.maxEntries) {
|
|
1151
1185
|
matches.push({
|
|
1152
1186
|
rule: "zip_too_many_entries",
|
|
1153
1187
|
severity: "medium",
|
|
1154
1188
|
meta: { seen, limit: cfg.maxEntries },
|
|
1155
1189
|
});
|
|
1190
|
+
// Return early — decompressing thousands of entries would be a DoS vector
|
|
1191
|
+
return matches;
|
|
1156
1192
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1193
|
+
// ── 3. True streaming decompression — archive bomb detection ────────────
|
|
1194
|
+
// For every DEFLATE entry (method=8) we feed the raw compressed bytes into
|
|
1195
|
+
// zlib.createInflateRaw() and count the bytes that come OUT. We abort the
|
|
1196
|
+
// moment any limit fires; we NEVER trust the header-reported uncompressed
|
|
1197
|
+
// size for the ratio decision.
|
|
1198
|
+
//
|
|
1199
|
+
// For STORED entries (method=0) compressed == uncompressed by spec, so the
|
|
1200
|
+
// byte count is immediate.
|
|
1201
|
+
let totalDecompressed = 0;
|
|
1202
|
+
for (const { lfhOffset, cdCompSize } of lfhIndex) {
|
|
1203
|
+
if (lfhOffset + 30 > buf.length)
|
|
1204
|
+
continue;
|
|
1205
|
+
if (r32(buf, lfhOffset) !== SIG_LFH)
|
|
1206
|
+
continue;
|
|
1207
|
+
const gpbf = r16(buf, lfhOffset + 6);
|
|
1208
|
+
const method = r16(buf, lfhOffset + 8);
|
|
1209
|
+
let lfhCompSz = r32(buf, lfhOffset + 18);
|
|
1210
|
+
const fnLen = r16(buf, lfhOffset + 26);
|
|
1211
|
+
const exLen = r16(buf, lfhOffset + 28);
|
|
1212
|
+
const dataOff = lfhOffset + 30 + fnLen + exLen;
|
|
1213
|
+
// If the data-descriptor flag is set (GPBF bit 3), the LFH sizes are 0.
|
|
1214
|
+
// Fall back to the CD size purely for navigation — not for bomb detection.
|
|
1215
|
+
if ((gpbf & 0x08) !== 0 && lfhCompSz === 0) {
|
|
1216
|
+
lfhCompSz = cdCompSize;
|
|
1217
|
+
}
|
|
1218
|
+
if (dataOff + lfhCompSz > buf.length)
|
|
1219
|
+
continue; // truncated entry — skip
|
|
1220
|
+
if (method === 8 /* DEFLATE */) {
|
|
1221
|
+
const compressed = buf.slice(dataOff, dataOff + lfhCompSz);
|
|
1222
|
+
const { decompressed, bombed } = await streamInflate(compressed, cfg.maxPerEntryUncompressedBytes, cfg.maxTotalUncompressedBytes, totalDecompressed, cfg.maxCompressionRatio);
|
|
1223
|
+
if (bombed)
|
|
1224
|
+
throw makeBombError();
|
|
1225
|
+
totalDecompressed += decompressed;
|
|
1226
|
+
}
|
|
1227
|
+
else if (method === 0 /* STORED */) {
|
|
1228
|
+
// Compressed == uncompressed for stored entries
|
|
1229
|
+
if (lfhCompSz > cfg.maxPerEntryUncompressedBytes)
|
|
1230
|
+
throw makeBombError();
|
|
1231
|
+
totalDecompressed += lfhCompSz;
|
|
1232
|
+
if (totalDecompressed > cfg.maxTotalUncompressedBytes)
|
|
1233
|
+
throw makeBombError();
|
|
1179
1234
|
}
|
|
1235
|
+
// Other methods (bzip2=12, lzma=14, zstd=93, …) — skip; no built-in support
|
|
1180
1236
|
}
|
|
1181
1237
|
return matches;
|
|
1182
1238
|
},
|