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 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="144" />
32
+ <img src="./assets/logo.svg" alt="Pompelmi logo" width="120" />
3
33
 
4
34
  <h1>Pompelmi</h1>
5
35
 
6
- <p><strong>Route-level upload security for Node.js.</strong></p>
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
- MIME and extension spoofing · archive abuse · risky document and binary
12
- signals · optional YARA
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
- <sub>Express · Next.js · NestJS · Fastify · Koa · Nuxt/Nitro · S3 quarantine flows · CI/CD</sub>
47
+ MIME spoofing · risky archives · document and binary signals · optional
48
+ YARA
19
49
  </p>
20
50
 
21
- <p><sub>Open-source core · MIT · Node.js 18+</sub></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://www.npmjs.com/package/pompelmi"><img alt="npm weekly downloads" src="https://img.shields.io/npm/dw/pompelmi" /></a>
29
- <a href="https://www.npmjs.com/package/pompelmi"><img alt="npm monthly downloads" src="https://img.shields.io/npm/dm/pompelmi" /></a>
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.github.io/pompelmi/getting-started/"><strong>Getting started</strong></a>
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.github.io/pompelmi/#browser-preview"><strong>Browser preview</strong></a>
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 <a href="https://nodeweekly.com/issues/594">Node Weekly</a>,
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://github.com/dzharii/awesome-typescript">Awesome TypeScript</a>.
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
- Minimal route-level example:
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.github.io/pompelmi/getting-started/) for a local scan in under a minute, open the [browser preview](https://pompelmi.github.io/pompelmi/#browser-preview) to inspect the verdict flow without sending files anywhere, or run the minimal [Express demo](./examples/demo).
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
- If Pompelmi matches how you want upload security to work, star the repo so more Node.js teams can find it.
131
+ ## Why teams use Pompelmi
86
132
 
87
- ## Why It Exists
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
- Upload endpoints are part of your attack surface. A file can look harmless at the form layer and become dangerous only after storage, extraction, rendering, or downstream parsing.
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
- Pompelmi keeps the first decision inside the application path, where the route still knows the file class, trust level, storage path, and failure mode.
139
+ ## What it checks
92
140
 
93
- ## What It Checks
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
- - MIME sniffing, magic-byte validation, and extension allowlists
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
- ## Where It Fits
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
- - public or semi-trusted upload endpoints that should inspect first and store later
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
- ## Why Not Just X?
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 | Filenames and client-reported MIME are easy to spoof |
114
- | Simple file-type or magic-byte checks | Confirming the file appears to be the claimed type | Risky internal structure, archive abuse, and route policy decisions |
115
- | Antivirus-only thinking | Known malicious matches and signature-based detection | Route context, spoofing checks, storage decisions, and non-signature risk signals |
116
- | Pompelmi at the upload route | Inspect-first, store-later decisions with policy, structure checks, and optional YARA | It is not a full antivirus replacement on its own |
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
- - Express: [Docs](https://pompelmi.github.io/pompelmi/how-to/express/) · [Minimal example](./examples/express-minimal) · [Demo](./examples/demo)
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
- ## Demo, Preview, and Examples
167
+ ## Supported frameworks and workflows
130
168
 
131
- ![Pompelmi upload security demo](assets/malware-detection-node-demo.gif)
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
- - [Browser preview](https://pompelmi.github.io/pompelmi/#browser-preview) for a fast local evaluation of the verdict UX
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
- ## Docs
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
+ ![Pompelmi upload security demo](assets/malware-detection-node-demo.gif)
146
183
 
147
- ## Enterprise and Commercial Support
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
- 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.github.io/pompelmi/enterprise/).
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
- ## Featured In
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
- - [Awesome JavaScript](https://github.com/sorrycc/awesome-javascript) — sorrycc
162
- - [Awesome TypeScript](https://github.com/dzharii/awesome-typescript) — dzharii
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
- ### Newsletters & Roundups
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
- - [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 (2026-02-23)
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
- *Found 9 mentions. To update, run `npm run mentions:update`.*
218
+ ## Commercial support
179
219
 
180
- <!-- MENTIONS:END -->
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: 1000,
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
- // Find EOCD near the end
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
- // Iterate central directory entries
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
- const sig = r32(buf, ptr);
1113
- if (sig !== SIG_CEN)
1114
- break; // stop if structure breaks
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 nameStart = ptr + 46;
1121
- const nameEnd = nameStart + fnLen;
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", nameStart, nameEnd);
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
- if (sumUnc > cfg.maxTotalUncompressedBytes) {
1158
- matches.push({
1159
- rule: "zip_total_uncompressed_too_large",
1160
- severity: "medium",
1161
- meta: { totalUncompressed: sumUnc, limit: cfg.maxTotalUncompressedBytes },
1162
- });
1163
- }
1164
- if (sumComp === 0 && sumUnc > 0) {
1165
- matches.push({
1166
- rule: "zip_suspicious_ratio",
1167
- severity: "medium",
1168
- meta: { ratio: Infinity },
1169
- });
1170
- }
1171
- else if (sumComp > 0) {
1172
- const ratio = sumUnc / Math.max(1, sumComp);
1173
- if (ratio >= cfg.maxCompressionRatio) {
1174
- matches.push({
1175
- rule: "zip_suspicious_ratio",
1176
- severity: "medium",
1177
- meta: { ratio, limit: cfg.maxCompressionRatio },
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
  },