@blamejs/core 0.8.43 → 0.8.49
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/CHANGELOG.md +92 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/guard-archive.js
CHANGED
|
@@ -1,124 +1,69 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* a later entry writes through the symlink's target. We refuse any
|
|
68
|
-
* entry whose extraction path passes THROUGH a symlink already in
|
|
69
|
-
* the entry list (when the operator passes pre-sorted entries).
|
|
70
|
-
*
|
|
71
|
-
* 5. Decompression-ratio bombs — per-entry compressedSize/size ratio
|
|
72
|
-
* cap (default: 100:1 strict, 1000:1 permissive). Aggregate ratio
|
|
73
|
-
* across all entries also capped.
|
|
74
|
-
*
|
|
75
|
-
* 6. Total-size cap — sum of uncompressed sizes (anti-DoS).
|
|
76
|
-
*
|
|
77
|
-
* 7. File-count cap — number of entries.
|
|
78
|
-
*
|
|
79
|
-
* 8. Nested-archive depth — refuses entries that are themselves
|
|
80
|
-
* archives unless `maxNestedDepth > 0`. Entry name suffixes are
|
|
81
|
-
* checked against an archive-extension catalog (.zip / .tar /
|
|
82
|
-
* .tar.gz / .tgz / .gz / .bz2 / .xz / .7z / .rar / .ar / .cpio /
|
|
83
|
-
* .lzma / .zst).
|
|
84
|
-
*
|
|
85
|
-
* 9. Per-entry-name validation via b.guardFilename — applies the full
|
|
86
|
-
* filename-safety catalog (path traversal / null-byte / Windows
|
|
87
|
-
* reserved names / NTFS ADS / RTLO bidi / overlong UTF-8 / shell-
|
|
88
|
-
* exec extensions / double-extension) to every entry's name.
|
|
89
|
-
*
|
|
90
|
-
* 10. Duplicate entry names — second entry with the same name silently
|
|
91
|
-
* overwrites the first on extraction. Refused.
|
|
92
|
-
*
|
|
93
|
-
* 11. Mixed-case duplicate names — case-insensitive collision on Windows
|
|
94
|
-
* / macOS HFS+ / APFS-non-case-sensitive volumes. Audited.
|
|
95
|
-
*
|
|
96
|
-
* 12. Encryption-claim mismatch — operator opts in to either "all
|
|
97
|
-
* entries encrypted" or "no entries encrypted"; mixing flagged.
|
|
98
|
-
*
|
|
99
|
-
* 13. Format-claim mismatch — `inspectMagic(buffer)` reads the first
|
|
100
|
-
* bytes and returns the detected format. Operator can compare
|
|
101
|
-
* against the declared content-type / extension; mismatch flagged.
|
|
102
|
-
*
|
|
103
|
-
* 14. Sparse archive (tar) — sparse entries can claim large
|
|
104
|
-
* uncompressed size with zero data; refused unless explicitly
|
|
105
|
-
* allowed.
|
|
106
|
-
*
|
|
107
|
-
* 15. Anti-DoS caps — total entry count, per-entry size, total size,
|
|
108
|
-
* compression ratio, recursion depth.
|
|
109
|
-
*
|
|
110
|
-
* Profiles:
|
|
111
|
-
* strict — every threat refused; no symlinks; no hardlinks;
|
|
112
|
-
* no nested archives; 100 entry max; 100 MiB total;
|
|
113
|
-
* 100:1 ratio cap; case-insensitive collision refused.
|
|
114
|
-
* balanced — symlinks within extraction-root allowed; no hardlinks;
|
|
115
|
-
* nested-depth 2; 10000 entries; 1 GiB total; 100:1
|
|
116
|
-
* per-entry / 1000:1 aggregate; case-collision audited.
|
|
117
|
-
* permissive — symlinks + hardlinks within root allowed; nested-depth
|
|
118
|
-
* 4; 100000 entries; 10 GiB total; 1000:1 ratio.
|
|
119
|
-
*
|
|
120
|
-
* Compliance postures: hipaa / pci-dss / gdpr / soc2 — strict
|
|
121
|
-
* overlay + forensic snapshots.
|
|
3
|
+
* @module b.guardArchive
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard Archive
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Archive content-safety guard — refuses hostile archive metadata
|
|
9
|
+
* BEFORE files touch the filesystem. Validates an operator-supplied
|
|
10
|
+
* entry list (the framework ships no pure-JS unzip / untar parser per
|
|
11
|
+
* the no-deps rule) plus an optional magic-byte inspection on raw
|
|
12
|
+
* bytes. Operators enumerate entries via their archive library
|
|
13
|
+
* (built-in zlib, OS tar / unzip CLI, vendored mupdf-of-archives) and
|
|
14
|
+
* pass `[{ name, size, compressedSize, isSymlink, isHardlink,
|
|
15
|
+
* linkTarget, isDirectory, isEncrypted, attrs }, ...]` to
|
|
16
|
+
* `validateEntries`.
|
|
17
|
+
*
|
|
18
|
+
* Zip-slip / path-traversal: entry names containing `..` segments,
|
|
19
|
+
* leading `/` or `\\`, or Windows drive-letter prefixes (`C:\\`) are
|
|
20
|
+
* refused under every profile. Composes `b.guardFilename` for the
|
|
21
|
+
* full leaf-safety catalog (null-byte, Windows reserved names, NTFS
|
|
22
|
+
* ADS, RTLO bidi, overlong UTF-8, shell-exec extensions, double-
|
|
23
|
+
* extension). Tracks the 2025-2026 CVE class: CVE-2025-3445
|
|
24
|
+
* (mholt/archiver), CVE-2025-32779 (EDDI), CVE-2025-62156 (Argo
|
|
25
|
+
* Workflows), CVE-2025-66945 (Zdir Pro), CVE-2025-45582 (GNU Tar
|
|
26
|
+
* two-step symlink bypass), CVE-2025-11001 / 11002 (7-Zip RCE),
|
|
27
|
+
* CVE-2025-4138 / 4517 (Python tarfile), CVE-2025-10854 (txtai),
|
|
28
|
+
* CVE-2025-12060 (Keras), CVE-2026-26960 (node-tar hardlink-via-
|
|
29
|
+
* symlink chain).
|
|
30
|
+
*
|
|
31
|
+
* Symlink / hardlink escape: entries whose `linkTarget` contains `..`
|
|
32
|
+
* or is absolute are refused. `strict` rejects symlinks AND hardlinks
|
|
33
|
+
* outright; `balanced` permits in-root symlinks and rejects hardlinks
|
|
34
|
+
* (CVE-2026-26960 class); `permissive` audits both.
|
|
35
|
+
*
|
|
36
|
+
* Decompression amplification: per-entry `compressedSize`/`size` ratio
|
|
37
|
+
* cap defaults 100:1 (strict) / 100:1 (balanced) / 1000:1 (permissive).
|
|
38
|
+
* Aggregate ratio across all entries also capped (`maxAggregateRatio`).
|
|
39
|
+
* Entry-count cap (`maxEntries`), per-entry size cap (`maxEntryBytes`),
|
|
40
|
+
* total uncompressed cap (`maxTotalBytes`).
|
|
41
|
+
*
|
|
42
|
+
* NTFS ADS, overlong UTF-8, leaf-bidi: routed through `b.guardFilename`
|
|
43
|
+
* on every entry name with `pathSeparatorsPolicy: "allow"` (archive
|
|
44
|
+
* entries legitimately use `/` as separator).
|
|
45
|
+
*
|
|
46
|
+
* Nested archives: entries with archive extensions (`.zip`, `.tar.gz`,
|
|
47
|
+
* `.7z`, `.rar`, `.zst`, ...) refused under `strict` (`maxNestedDepth:
|
|
48
|
+
* 0`); audited under `balanced` (depth 2) / `permissive` (depth 4) so
|
|
49
|
+
* the operator can recurse.
|
|
50
|
+
*
|
|
51
|
+
* Duplicate-name + case-insensitive collision detection — the second
|
|
52
|
+
* entry with the same name silently overwrites on extraction (refused);
|
|
53
|
+
* case-insensitive collisions on Windows / HFS+ / APFS-non-case-
|
|
54
|
+
* sensitive volumes (audited / refused per profile).
|
|
55
|
+
*
|
|
56
|
+
* `inspectMagic(buffer)` returns `{ format, magic }` for ZIP / GZIP /
|
|
57
|
+
* BZIP2 / XZ / 7Z / RAR4 / RAR5 / LZMA / ZSTD / TAR (the latter via
|
|
58
|
+
* the "ustar" magic at offset 257). `checkExtractionPath(name, root)`
|
|
59
|
+
* provides a single-entry boolean for callers that already enumerate.
|
|
60
|
+
*
|
|
61
|
+
* Profiles `strict` / `balanced` / `permissive` and compliance
|
|
62
|
+
* postures `hipaa` / `pci-dss` / `gdpr` / `soc2` overlay on the
|
|
63
|
+
* profile baseline.
|
|
64
|
+
*
|
|
65
|
+
* @card
|
|
66
|
+
* Archive content-safety guard — refuses hostile archive metadata BEFORE files touch the filesystem.
|
|
122
67
|
*/
|
|
123
68
|
|
|
124
69
|
var lazyRequire = require("./lazy-require");
|
|
@@ -296,8 +241,33 @@ function _bufferStartsWith(buf, sig) {
|
|
|
296
241
|
return true;
|
|
297
242
|
}
|
|
298
243
|
|
|
299
|
-
|
|
300
|
-
|
|
244
|
+
/**
|
|
245
|
+
* @primitive b.guardArchive.inspectMagic
|
|
246
|
+
* @signature b.guardArchive.inspectMagic(buffer)
|
|
247
|
+
* @since 0.7.8
|
|
248
|
+
* @status stable
|
|
249
|
+
* @related b.guardArchive.validateEntries, b.guardArchive.gate
|
|
250
|
+
*
|
|
251
|
+
* Read the first bytes of `buffer` and return
|
|
252
|
+
* `{ format, magic }` when the buffer matches a known archive-format
|
|
253
|
+
* signature (`zip` / `gzip` / `bzip2` / `xz` / `7z` / `rar4` / `rar5` /
|
|
254
|
+
* `lzma` / `zstd` / `tar`). TAR is detected via the `"ustar"` magic
|
|
255
|
+
* at offset 257 within the first 512-byte header block. Returns
|
|
256
|
+
* `null` on unrecognized input or non-Buffer / empty input. Pure
|
|
257
|
+
* inspection — never mutates the buffer or throws.
|
|
258
|
+
*
|
|
259
|
+
* Operators compare the detected format against the declared
|
|
260
|
+
* Content-Type / extension to surface format-claim mismatches before
|
|
261
|
+
* routing the bytes to a parser.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* var zipBytes = Buffer.from([0x50, 0x4B, 0x03, 0x04, 0x14, 0x00]);
|
|
265
|
+
* var hit = b.guardArchive.inspectMagic(zipBytes);
|
|
266
|
+
* hit.format; // → "zip"
|
|
267
|
+
*
|
|
268
|
+
* var noise = Buffer.from([0x00, 0x01, 0x02, 0x03]);
|
|
269
|
+
* b.guardArchive.inspectMagic(noise); // → null
|
|
270
|
+
*/
|
|
301
271
|
function inspectMagic(buffer) {
|
|
302
272
|
if (!Buffer.isBuffer(buffer) || buffer.length === 0) return null;
|
|
303
273
|
for (var i = 0; i < MAGIC_SIGNATURES.length; i += 1) {
|
|
@@ -318,9 +288,35 @@ function inspectMagic(buffer) {
|
|
|
318
288
|
return null;
|
|
319
289
|
}
|
|
320
290
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
291
|
+
/**
|
|
292
|
+
* @primitive b.guardArchive.checkExtractionPath
|
|
293
|
+
* @signature b.guardArchive.checkExtractionPath(entryName, extractionRoot)
|
|
294
|
+
* @since 0.7.8
|
|
295
|
+
* @status stable
|
|
296
|
+
* @related b.guardArchive.validateEntries, b.guardArchive.gate
|
|
297
|
+
*
|
|
298
|
+
* Single-entry boolean check: returns `{ ok, reason }` for a candidate
|
|
299
|
+
* `(entryName, extractionRoot)` pair. Refuses entries whose name
|
|
300
|
+
* contains a `..` component (zip slip — CVE-2025-3445 class), is an
|
|
301
|
+
* absolute path (leading `/`, `\\`, or `C:\\` drive-letter prefix),
|
|
302
|
+
* carries a null byte, or is empty. The framework cannot resolve
|
|
303
|
+
* `path.resolve(extractionRoot, entryName)` without a `node:path`
|
|
304
|
+
* coupling that the gate keeps portable; the operator's extraction
|
|
305
|
+
* code is expected to additionally call `path.resolve` and confirm
|
|
306
|
+
* the result starts with `path.resolve(extractionRoot)`.
|
|
307
|
+
*
|
|
308
|
+
* Use when the operator already enumerates archive entries and wants
|
|
309
|
+
* a per-call boolean rather than running the full
|
|
310
|
+
* `validateEntries` issue list.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* b.guardArchive.checkExtractionPath("docs/readme.txt", "/var/extract").ok;
|
|
314
|
+
* // → true
|
|
315
|
+
*
|
|
316
|
+
* var bad = b.guardArchive.checkExtractionPath("../etc/passwd", "/var/extract");
|
|
317
|
+
* bad.ok; // → false
|
|
318
|
+
* bad.reason; // → "entry name contains .. component (zip slip)"
|
|
319
|
+
*/
|
|
324
320
|
function checkExtractionPath(entryName, extractionRoot) {
|
|
325
321
|
if (typeof entryName !== "string" || entryName.length === 0) {
|
|
326
322
|
return { ok: false, reason: "empty entry name" };
|
|
@@ -630,6 +626,64 @@ function _detectIssues(entries, opts) {
|
|
|
630
626
|
|
|
631
627
|
// ---- Public surface ----
|
|
632
628
|
|
|
629
|
+
/**
|
|
630
|
+
* @primitive b.guardArchive.validateEntries
|
|
631
|
+
* @signature b.guardArchive.validateEntries(entries, opts)
|
|
632
|
+
* @since 0.7.8
|
|
633
|
+
* @status stable
|
|
634
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
635
|
+
* @related b.guardArchive.gate, b.guardArchive.inspectMagic, b.guardFilename.validate
|
|
636
|
+
*
|
|
637
|
+
* Inspect an operator-supplied `entries` array (one entry per archive
|
|
638
|
+
* member: `{ name, size, compressedSize, isSymlink, isHardlink,
|
|
639
|
+
* linkTarget, isDirectory, isEncrypted, attrs }`) and return
|
|
640
|
+
* `{ ok, issues }`. Issues carry `{ kind, severity, ruleId, location,
|
|
641
|
+
* snippet }` with severity `"warn"` / `"high"` / `"critical"`.
|
|
642
|
+
* Detected: zip-slip, absolute path, symlink / hardlink escape,
|
|
643
|
+
* compression-ratio bombs (per-entry + aggregate), per-entry size
|
|
644
|
+
* cap, total-size cap, entry-count cap, nested-archive entries,
|
|
645
|
+
* duplicate names, case-insensitive collisions, encryption-claim
|
|
646
|
+
* mismatch, sparse-tar entries, plus the full `b.guardFilename`
|
|
647
|
+
* leaf-safety catalog re-attached with archive-context locations.
|
|
648
|
+
* Pure inspection — never mutates input or throws on hostile entries.
|
|
649
|
+
*
|
|
650
|
+
* @opts
|
|
651
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
652
|
+
* compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
653
|
+
* traversalPolicy: "reject"|"audit"|"allow",
|
|
654
|
+
* absolutePathPolicy: "reject"|"audit"|"allow",
|
|
655
|
+
* symlinkPolicy: "reject"|"audit"|"allow",
|
|
656
|
+
* hardlinkPolicy: "reject"|"audit"|"allow",
|
|
657
|
+
* nestedArchivePolicy: "reject"|"audit"|"allow",
|
|
658
|
+
* duplicateNamePolicy: "reject"|"audit"|"allow",
|
|
659
|
+
* caseInsensitiveCollisionPolicy: "reject"|"audit"|"allow",
|
|
660
|
+
* encryptionPolicy: "reject"|"audit"|"allow",
|
|
661
|
+
* sparseEntryPolicy: "reject"|"audit"|"allow",
|
|
662
|
+
* filenameProfile: "balanced"|"strict"|"permissive",
|
|
663
|
+
* maxEntries: number, // strict 100, balanced 10000, permissive 100000
|
|
664
|
+
* maxTotalBytes: number, // strict 100 MiB, balanced 1 GiB, permissive 10 GiB
|
|
665
|
+
* maxEntryBytes: number, // strict 50 MiB, balanced 500 MiB, permissive 2 GiB
|
|
666
|
+
* maxCompressionRatio: number, // strict / balanced 100, permissive 1000
|
|
667
|
+
* maxAggregateRatio: number, // strict 200, balanced 1000, permissive 10000
|
|
668
|
+
* maxNestedDepth: number, // strict 0, balanced 2, permissive 4
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* var rv = b.guardArchive.validateEntries([
|
|
672
|
+
* { name: "docs/readme.txt", size: 1000, compressedSize: 500 },
|
|
673
|
+
* { name: "../etc/passwd", size: 100, compressedSize: 50 },
|
|
674
|
+
* ], { profile: "strict" });
|
|
675
|
+
* rv.ok; // → false
|
|
676
|
+
* rv.issues[0].kind; // → "zip-slip"
|
|
677
|
+
* rv.issues[0].severity; // → "critical"
|
|
678
|
+
*
|
|
679
|
+
* // Compression-ratio bomb — 50 MiB uncompressed from 50 KiB compressed
|
|
680
|
+
* // is 1000:1, far above the 100:1 strict cap.
|
|
681
|
+
* var bomb = b.guardArchive.validateEntries([
|
|
682
|
+
* { name: "bomb.bin", size: 52428800, compressedSize: 51200 },
|
|
683
|
+
* ], { profile: "strict" });
|
|
684
|
+
* bomb.issues.some(function (i) { return i.kind === "compression-ratio-bomb"; });
|
|
685
|
+
* // → true
|
|
686
|
+
*/
|
|
633
687
|
function validateEntries(entries, opts) {
|
|
634
688
|
opts = _resolveOpts(opts);
|
|
635
689
|
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
@@ -646,6 +700,49 @@ function validateEntries(entries, opts) {
|
|
|
646
700
|
return gateContract.aggregateIssues(_detectIssues(entries, opts));
|
|
647
701
|
}
|
|
648
702
|
|
|
703
|
+
/**
|
|
704
|
+
* @primitive b.guardArchive.gate
|
|
705
|
+
* @signature b.guardArchive.gate(opts)
|
|
706
|
+
* @since 0.7.8
|
|
707
|
+
* @status stable
|
|
708
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
709
|
+
* @related b.guardArchive.validateEntries, b.guardArchive.inspectMagic, b.fileUpload, b.staticServe
|
|
710
|
+
*
|
|
711
|
+
* Build a `b.gateContract` gate suitable for `b.fileUpload({ contentSafety:
|
|
712
|
+
* { "application/zip": gate } })` or `b.staticServe`. Operators pass
|
|
713
|
+
* `ctx.entries` (the enumerated entry list from their archive library)
|
|
714
|
+
* — when only `ctx.bytes` is supplied, the gate runs `inspectMagic` to
|
|
715
|
+
* confirm the format and refuses with a `"no-entry-list"` issue
|
|
716
|
+
* directing the operator to enumerate entries explicitly (the
|
|
717
|
+
* framework ships no parser for any archive format).
|
|
718
|
+
*
|
|
719
|
+
* Action chain: `serve` (no issues) → `audit-only` (warn-only) →
|
|
720
|
+
* `refuse` (any critical/high). Archive content has no safe
|
|
721
|
+
* sanitization — there is no `sanitize` action in the chain.
|
|
722
|
+
*
|
|
723
|
+
* @opts
|
|
724
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
725
|
+
* compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
726
|
+
* name: string,
|
|
727
|
+
* ...: any validateEntries opt
|
|
728
|
+
*
|
|
729
|
+
* @example
|
|
730
|
+
* var archiveGate = b.guardArchive.gate({ profile: "strict" });
|
|
731
|
+
*
|
|
732
|
+
* var verdict = await archiveGate.check({
|
|
733
|
+
* entries: [
|
|
734
|
+
* { name: "docs/readme.txt", size: 1000, compressedSize: 500 },
|
|
735
|
+
* { name: "../etc/passwd", size: 100, compressedSize: 50 },
|
|
736
|
+
* ],
|
|
737
|
+
* });
|
|
738
|
+
* verdict.action; // → "refuse"
|
|
739
|
+
*
|
|
740
|
+
* // Bytes-only call without an entry list — operator must enumerate.
|
|
741
|
+
* var zipBytes = Buffer.from([0x50, 0x4B, 0x03, 0x04, 0x14, 0x00]);
|
|
742
|
+
* var v2 = await archiveGate.check({ bytes: zipBytes });
|
|
743
|
+
* v2.action; // → "refuse"
|
|
744
|
+
* v2.issues[0].kind; // → "no-entry-list"
|
|
745
|
+
*/
|
|
649
746
|
function gate(opts) {
|
|
650
747
|
opts = _resolveOpts(opts);
|
|
651
748
|
return gateContract.buildGuardGate(
|
|
@@ -689,13 +786,78 @@ function gate(opts) {
|
|
|
689
786
|
});
|
|
690
787
|
}
|
|
691
788
|
|
|
789
|
+
/**
|
|
790
|
+
* @primitive b.guardArchive.buildProfile
|
|
791
|
+
* @signature b.guardArchive.buildProfile(opts)
|
|
792
|
+
* @since 0.7.8
|
|
793
|
+
* @status stable
|
|
794
|
+
* @related b.guardArchive.compliancePosture, b.guardArchive.gate
|
|
795
|
+
*
|
|
796
|
+
* Resolve a named profile against the guard's PROFILES catalog and
|
|
797
|
+
* return the merged options bag. Operators introspecting the active
|
|
798
|
+
* caps (without calling `validateEntries` / `gate`) use this. Throws
|
|
799
|
+
* `GuardArchiveError("archive.bad-profile")` on unknown name.
|
|
800
|
+
*
|
|
801
|
+
* @opts
|
|
802
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* var resolved = b.guardArchive.buildProfile({ profile: "strict" });
|
|
806
|
+
* resolved.maxEntries; // → 100
|
|
807
|
+
* resolved.symlinkPolicy; // → "reject"
|
|
808
|
+
* resolved.maxCompressionRatio; // → 100
|
|
809
|
+
*/
|
|
692
810
|
var buildProfile = gateContract.makeProfileBuilder(PROFILES);
|
|
693
811
|
|
|
812
|
+
/**
|
|
813
|
+
* @primitive b.guardArchive.compliancePosture
|
|
814
|
+
* @signature b.guardArchive.compliancePosture(name)
|
|
815
|
+
* @since 0.7.8
|
|
816
|
+
* @status stable
|
|
817
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
818
|
+
* @related b.guardArchive.gate, b.guardArchive.buildProfile
|
|
819
|
+
*
|
|
820
|
+
* Return the option overlay for a named compliance posture
|
|
821
|
+
* (`"hipaa"` / `"pci-dss"` / `"gdpr"` / `"soc2"`). Composes over a
|
|
822
|
+
* base profile to harden defaults per regulatory regime. Throws
|
|
823
|
+
* `GuardArchiveError("archive.bad-posture")` on unknown name.
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* var posture = b.guardArchive.compliancePosture("hipaa");
|
|
827
|
+
* posture.symlinkPolicy; // → "reject"
|
|
828
|
+
* posture.hardlinkPolicy; // → "reject"
|
|
829
|
+
* posture.forensicSnippetBytes; // → 256
|
|
830
|
+
*/
|
|
694
831
|
function compliancePosture(name) {
|
|
695
832
|
return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES, _err, "archive");
|
|
696
833
|
}
|
|
697
834
|
|
|
698
835
|
var _archiveRulePacks = gateContract.makeRulePackLoader(GuardArchiveError, "archive");
|
|
836
|
+
/**
|
|
837
|
+
* @primitive b.guardArchive.loadRulePack
|
|
838
|
+
* @signature b.guardArchive.loadRulePack(pack)
|
|
839
|
+
* @since 0.7.8
|
|
840
|
+
* @status stable
|
|
841
|
+
* @related b.guardArchive.gate
|
|
842
|
+
*
|
|
843
|
+
* Register an operator-supplied rule pack with the guard-archive
|
|
844
|
+
* registry. The pack is identified by `pack.id` (non-empty string)
|
|
845
|
+
* and stored for later inspection / dispatch by gates that opt in
|
|
846
|
+
* via `opts.rulePackId`. Returns the pack object unchanged on
|
|
847
|
+
* success; throws `GuardArchiveError("archive.bad-opt")` when
|
|
848
|
+
* `pack` is missing or `pack.id` is not a non-empty string.
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* var pack = b.guardArchive.loadRulePack({
|
|
852
|
+
* id: "kb-2026-archive",
|
|
853
|
+
* extraReservedNames: ["system32"],
|
|
854
|
+
* rules: [
|
|
855
|
+
* { id: "no-windows-system", severity: "critical",
|
|
856
|
+
* reason: "entry name targets Windows system directory" },
|
|
857
|
+
* ],
|
|
858
|
+
* });
|
|
859
|
+
* pack.id; // → "kb-2026-archive"
|
|
860
|
+
*/
|
|
699
861
|
var loadRulePack = _archiveRulePacks.load;
|
|
700
862
|
|
|
701
863
|
module.exports = {
|