@blamejs/core 0.8.42 → 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 +93 -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-filename.js
CHANGED
|
@@ -1,88 +1,67 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.guardFilename
|
|
4
|
+
* @nav Guards
|
|
5
|
+
* @title Guard Filename
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Filename content-safety primitive (KIND="filename"). Validates
|
|
9
|
+
* user-supplied filenames before they reach disk, network paths,
|
|
10
|
+
* or Content-Disposition headers. Standalone primitive — does NOT
|
|
11
|
+
* register into `b.guardAll`'s content-type-routed dispatch (no
|
|
12
|
+
* canonical mime / ext); operators wire it directly via
|
|
13
|
+
* `b.fileUpload({ filenameSafety: gate })` and similar host opts.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* 2. Null-byte truncation — `file.txt\x00.exe` — string ends at null
|
|
23
|
-
* in C-shaped APIs while validation sees `.txt`. Refused.
|
|
24
|
-
*
|
|
25
|
-
* 3. Windows reserved device names — CON / PRN / AUX / NUL / COM1-9 /
|
|
26
|
-
* LPT1-9 / CLOCK$ — even with extensions (CON.txt is reserved).
|
|
27
|
-
* Case-insensitive match. Refused.
|
|
28
|
-
*
|
|
29
|
-
* 4. NTFS alternate data streams — `file.txt:hidden.exe`. Colon is
|
|
30
|
-
* the ADS separator on NTFS. Refused.
|
|
31
|
-
*
|
|
32
|
-
* 5. Leading/trailing whitespace + trailing dots — Windows strips
|
|
33
|
-
* them silently, so `secret.txt ` and `secret.txt.` save as
|
|
34
|
-
* `secret.txt`. Refused under strict, stripped under balanced.
|
|
35
|
-
*
|
|
36
|
-
* 6. Unicode bidi / RTLO — CVE-2021-42574 in filename context. The
|
|
37
|
-
* Memento-RTLO toolkit weaponizes this: `Photo01Bygpj.SCR`
|
|
38
|
-
* displays as `Photo01ByRCS.jpg` while the OS opens `.SCR`.
|
|
39
|
-
* Refused regardless of profile.
|
|
40
|
-
*
|
|
41
|
-
* 7. Zero-width / invisible-formatting chars — used to hide the real
|
|
42
|
-
* extension between the visible name and what the OS sees.
|
|
43
|
-
*
|
|
44
|
-
* 8. Homoglyph chars — Cyrillic / Greek / fullwidth Latin mixed with
|
|
45
|
-
* ASCII letters in a single name. Operator-decided severity per
|
|
46
|
-
* profile.
|
|
15
|
+
* Path-traversal defense: `..` / `../` / `..\\`, percent-encoded
|
|
16
|
+
* `%2e%2e`, double-encoded `%252e%252e`, and UTF-8 overlong
|
|
17
|
+
* sequences `0xC0 0xAE` (for `.`) and `0xC0 0xAF` (for `/`) ALWAYS
|
|
18
|
+
* throw — no profile downgrades the refusal. Threat catalog
|
|
19
|
+
* grounded in OWASP Path Traversal + WSTG file-inclusion testing
|
|
20
|
+
* guides; CWE-22 / 23 / 35 / 73 / 78 / 434 / 36; PortSwigger
|
|
21
|
+
* File-path-traversal series (null-byte bypass + extension
|
|
22
|
+
* validation); Memento-RTLO + RTL-Spiegel filename-spoofing
|
|
23
|
+
* reports (CVE-2021-42574 in filename context); Kevin Boone
|
|
24
|
+
* overlong UTF-8 write-up.
|
|
47
25
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
26
|
+
* Universal-throw security floor: null-byte truncation
|
|
27
|
+
* (`file.txt\x00.exe`), NTFS alternate data streams
|
|
28
|
+
* (`file.txt:hidden.exe`), UNC paths (`\\server\share\file` and
|
|
29
|
+
* `//host/share/file`), and overlong UTF-8 byte sequences ALL
|
|
30
|
+
* throw `GuardFilenameError` regardless of profile — there is no
|
|
31
|
+
* sanitize-action that repairs these classes. Windows reserved
|
|
32
|
+
* device names (CON / PRN / AUX / NUL / COM1-9 / LPT1-9 / CLOCK$
|
|
33
|
+
* / CONFIG$) refuse under strict and balanced (even with
|
|
34
|
+
* extensions — `CON.txt` collides with the device).
|
|
50
35
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
36
|
+
* Unicode hygiene: BIDI / RTLO refuses at every profile (Memento-
|
|
37
|
+
* RTLO `Photo01Bygpj.SCR` displays as `Photo01ByRCS.jpg` while
|
|
38
|
+
* the OS opens `.SCR`). Zero-width and invisible-formatting strip
|
|
39
|
+
* under balanced/permissive, refuse under strict. Homoglyph
|
|
40
|
+
* (Cyrillic / Greek / fullwidth Latin mixed with ASCII letters)
|
|
41
|
+
* refuses under strict, audits under balanced/permissive.
|
|
54
42
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
43
|
+
* Extension policy: operator-supplied `extensionAllowlist`
|
|
44
|
+
* catches double-extension bypass (`file.jpg.exe` lands at the
|
|
45
|
+
* last `.exe` and refuses). Shell-shortcut / executable extensions
|
|
46
|
+
* (`.lnk` / `.url` / `.desktop` / `.scr` / `.bat` / `.cmd` /
|
|
47
|
+
* `.com` / `.pif` / `.vbs` / `.js` / `.jse` / `.wsf` / `.wsh` /
|
|
48
|
+
* `.ps1` / `.psm1` / `.app` / `.deb` / `.rpm` / `.msi` and the
|
|
49
|
+
* broader native-binary family) refuse under strict, audit under
|
|
50
|
+
* balanced/permissive.
|
|
57
51
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
52
|
+
* Length caps: 64 bytes (strict), 255 bytes (balanced/permissive).
|
|
53
|
+
* Path separators in the leaf refuse under strict/balanced;
|
|
54
|
+
* permissive opts in to multi-component paths via
|
|
55
|
+
* `pathSeparatorsPolicy: "audit"` and `maxComponents > 1`.
|
|
60
56
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
57
|
+
* Profiles: `strict` / `balanced` / `permissive`. Compliance
|
|
58
|
+
* postures: `hipaa` / `pci-dss` / `gdpr` / `soc2`. Threat-detection
|
|
59
|
+
* regex literals composed programmatically from numeric codepoint
|
|
60
|
+
* range tables (`lib/codepoint-class`); source file never embeds
|
|
61
|
+
* attack characters.
|
|
63
62
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* 15. Allowlist mode — operators can pass `extensionAllowlist:
|
|
67
|
-
* [".png", ".jpg", ".pdf"]` to require a single allowed extension.
|
|
68
|
-
* The validator catches double-extension bypass: `file.jpg.exe`
|
|
69
|
-
* lands at the last dot's extension `.exe` and is refused.
|
|
70
|
-
*
|
|
71
|
-
* 16. Shell-shortcut extensions — `.lnk`, `.url`, `.desktop`, `.scr`,
|
|
72
|
-
* `.bat`, `.cmd`, `.com`, `.pif`, `.vbs`, `.js`, `.jse`, `.wsf`,
|
|
73
|
-
* `.wsh`, `.ps1`, `.psm1`, `.app`, `.deb`, `.rpm`, `.msi`. Refused
|
|
74
|
-
* under strict; balanced/permissive emit a warn-level audit issue.
|
|
75
|
-
*
|
|
76
|
-
* 17. UTF-8 overlong encoding — bytes that decode to ASCII separators
|
|
77
|
-
* via non-shortest-form encoding (Unicode standard prohibits, but
|
|
78
|
-
* legacy decoders accept them).
|
|
79
|
-
*
|
|
80
|
-
* 18. Anti-DoS caps — total filename byte length, total component
|
|
81
|
-
* count when the operator allows path-shape (default: leaf only).
|
|
82
|
-
*
|
|
83
|
-
* Threat-detection regex literals composed programmatically from numeric
|
|
84
|
-
* codepoint range tables (lib/codepoint-class). Source file never
|
|
85
|
-
* embeds attack characters.
|
|
63
|
+
* @card
|
|
64
|
+
* Filename content-safety primitive (KIND="filename").
|
|
86
65
|
*/
|
|
87
66
|
|
|
88
67
|
var codepointClass = require("./codepoint-class");
|
|
@@ -623,6 +602,56 @@ function _sanitize(input, opts) {
|
|
|
623
602
|
|
|
624
603
|
// ---- Public surface ----
|
|
625
604
|
|
|
605
|
+
/**
|
|
606
|
+
* @primitive b.guardFilename.validate
|
|
607
|
+
* @signature b.guardFilename.validate(input, opts?)
|
|
608
|
+
* @since 0.7.5
|
|
609
|
+
* @status stable
|
|
610
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
611
|
+
* @related b.guardFilename.sanitize, b.guardFilename.gate
|
|
612
|
+
*
|
|
613
|
+
* Inspect a filename (string or Buffer) and return
|
|
614
|
+
* `{ ok, issues, summary }`. Each issue carries
|
|
615
|
+
* `{ kind, severity, ruleId, location, snippet }` with severity in
|
|
616
|
+
* `"warn"|"high"|"critical"`. Detected: path-traversal raw and
|
|
617
|
+
* percent-encoded, null-byte truncation, NTFS ADS, UNC path,
|
|
618
|
+
* overlong UTF-8, Windows reserved-name, reserved character,
|
|
619
|
+
* leading/trailing whitespace + trailing dot, BIDI / control /
|
|
620
|
+
* zero-width / homoglyph, non-ASCII (when `requireAscii`), length
|
|
621
|
+
* cap, multi-dot violation, extension allowlist miss, double-
|
|
622
|
+
* extension with executable last segment, shell-shortcut extension.
|
|
623
|
+
* Pure inspection — never throws.
|
|
624
|
+
*
|
|
625
|
+
* @opts
|
|
626
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
627
|
+
* compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
628
|
+
* bidiPolicy: "reject"|"strip"|"allow",
|
|
629
|
+
* controlPolicy: "reject"|"strip"|"allow",
|
|
630
|
+
* nullBytePolicy: "reject", // always reject
|
|
631
|
+
* zeroWidthPolicy: "reject"|"strip"|"allow",
|
|
632
|
+
* homoglyphPolicy: "reject"|"audit"|"allow",
|
|
633
|
+
* traversalPolicy: "reject", // always reject
|
|
634
|
+
* reservedCharPolicy: "reject"|"strip"|"allow",
|
|
635
|
+
* reservedNamePolicy: "reject"|"audit"|"allow",
|
|
636
|
+
* adsPolicy: "reject", // always reject
|
|
637
|
+
* leadingTrailingPolicy: "reject"|"strip"|"allow",
|
|
638
|
+
* shellExecExtPolicy: "reject"|"audit"|"allow",
|
|
639
|
+
* pathSeparatorsPolicy: "reject"|"audit"|"allow",
|
|
640
|
+
* unicodeNormalization: "NFC"|null,
|
|
641
|
+
* requireAscii: boolean,
|
|
642
|
+
* extensionAllowlist: string[]|null,
|
|
643
|
+
* requireSingleDot: boolean,
|
|
644
|
+
* maxBytes: number, // leaf-name byte cap
|
|
645
|
+
* maxComponents: number, // path-component count
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* var rv = b.guardFilename.validate("../etc/passwd", { profile: "strict" });
|
|
649
|
+
* rv.ok; // → false
|
|
650
|
+
* rv.issues.some(function (i) { return i.kind === "path-traversal"; }); // → true
|
|
651
|
+
*
|
|
652
|
+
* var ok = b.guardFilename.validate("report-2026-Q1.txt", { profile: "strict" });
|
|
653
|
+
* ok.ok; // → true
|
|
654
|
+
*/
|
|
626
655
|
function validate(input, opts) {
|
|
627
656
|
opts = _resolveOpts(opts);
|
|
628
657
|
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
@@ -634,14 +663,151 @@ function validate(input, opts) {
|
|
|
634
663
|
return gateContract.aggregateIssues(_detectIssues(input, opts));
|
|
635
664
|
}
|
|
636
665
|
|
|
666
|
+
function _sanitizeStripMode(input, opts) {
|
|
667
|
+
// mode: "strip" — operator-friendly Content-Disposition path. C0 control
|
|
668
|
+
// chars (CR / LF / etc., excluding NUL — null-byte truncation is never
|
|
669
|
+
// sanitizable) and bidi-override codepoints are replaced with "_". The
|
|
670
|
+
// security floor still applies: path traversal, null-byte, NTFS ADS,
|
|
671
|
+
// UNC, and overlong UTF-8 throw at every profile level.
|
|
672
|
+
if (Buffer.isBuffer(input) && _hasOverlongUtf8(input)) {
|
|
673
|
+
throw _err("filename.overlong-utf8", "filename has overlong UTF-8 sequence — cannot sanitize");
|
|
674
|
+
}
|
|
675
|
+
var name = typeof input === "string"
|
|
676
|
+
? input
|
|
677
|
+
: (Buffer.isBuffer(input) ? input.toString("utf8") : "");
|
|
678
|
+
if (name.length === 0) {
|
|
679
|
+
throw _err("filename.empty", "sanitize requires non-empty filename");
|
|
680
|
+
}
|
|
681
|
+
if (name.indexOf("\0") !== -1) {
|
|
682
|
+
throw _err("filename.null-byte", "filename contains null byte — null-byte truncation is never sanitizable");
|
|
683
|
+
}
|
|
684
|
+
// Replace control chars + bidi codepoints with "_". Zero-width is
|
|
685
|
+
// also stripped (visible-name spoofing has no value if the bytes
|
|
686
|
+
// round-trip silently). CR (U+0D) / LF (U+0A) are NOT in the shared
|
|
687
|
+
// C0 table (they're dialect-shaped chars elsewhere) but they're the
|
|
688
|
+
// exact chars that enable Content-Disposition response splitting,
|
|
689
|
+
// which is the primary use case for strip mode — replace explicitly.
|
|
690
|
+
// allow:dynamic-regex — replace-character class composed at construction
|
|
691
|
+
name = name.replace(/[\r\n\t\v\f]/g, "_");
|
|
692
|
+
name = name.replace(codepointClass.C0_CTRL_RE_G, "_");
|
|
693
|
+
name = name.replace(codepointClass.BIDI_RE_G, "_");
|
|
694
|
+
name = name.replace(codepointClass.ZW_RE_G, "_");
|
|
695
|
+
if (opts.unicodeNormalization === "NFC") name = _normalizeNFC(name);
|
|
696
|
+
|
|
697
|
+
// Security floor — never sanitizable.
|
|
698
|
+
// allow:regex-no-length-cap — operator-supplied filename; length validated below
|
|
699
|
+
if (PATH_TRAVERSAL_RE.test(name) || PERCENT_ENCODED_TRAVERSAL_RE.test(name) ||
|
|
700
|
+
name === "." || name === "..") {
|
|
701
|
+
throw _err("filename.traversal", "filename contains path-traversal sequence");
|
|
702
|
+
}
|
|
703
|
+
if (/^\\\\|^\/\//.test(name)) {
|
|
704
|
+
throw _err("filename.unc", "UNC path syntax");
|
|
705
|
+
}
|
|
706
|
+
if (/:[^:\\/]+$/.test(name) && name.charAt(0) !== "/") {
|
|
707
|
+
throw _err("filename.ntfs-ads", "filename contains NTFS alternate data stream syntax");
|
|
708
|
+
}
|
|
709
|
+
if (Buffer.byteLength(name, "utf8") > opts.maxBytes) {
|
|
710
|
+
throw _err("filename.length", "filename exceeds maxBytes " + opts.maxBytes);
|
|
711
|
+
}
|
|
712
|
+
if (name.length === 0) {
|
|
713
|
+
throw _err("filename.empty", "sanitize produced empty filename");
|
|
714
|
+
}
|
|
715
|
+
return name;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* @primitive b.guardFilename.sanitize
|
|
720
|
+
* @signature b.guardFilename.sanitize(input, opts?)
|
|
721
|
+
* @since 0.7.5
|
|
722
|
+
* @status stable
|
|
723
|
+
* @related b.guardFilename.validate, b.guardFilename.gate
|
|
724
|
+
*
|
|
725
|
+
* Best-effort cleanup of a filename. Two modes: `"enforce"` (default;
|
|
726
|
+
* applies the profile's strip/reject policies and throws on
|
|
727
|
+
* unsanitizable refusals) and `"strip"` (operator-friendly
|
|
728
|
+
* Content-Disposition path — replaces control / bidi / zero-width
|
|
729
|
+
* codepoints with `_` and applies a security floor).
|
|
730
|
+
*
|
|
731
|
+
* The security floor ALWAYS throws regardless of mode/profile:
|
|
732
|
+
* path-traversal raw and percent-encoded, null-byte, NTFS alternate
|
|
733
|
+
* data streams, UNC paths, overlong UTF-8 sequences, and post-strip
|
|
734
|
+
* length-cap violation. These classes are unrepairable — silently
|
|
735
|
+
* fixing them would mask the attack signal an audit log needs.
|
|
736
|
+
*
|
|
737
|
+
* @opts
|
|
738
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
739
|
+
* compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
740
|
+
* mode: "enforce"|"strip",
|
|
741
|
+
* audit: { safeEmit: function }, // optional sink for strip mode
|
|
742
|
+
* unicodeNormalization: "NFC"|null,
|
|
743
|
+
* maxBytes: number,
|
|
744
|
+
*
|
|
745
|
+
* @example
|
|
746
|
+
* var safe = b.guardFilename.sanitize("My File.txt", { profile: "balanced" });
|
|
747
|
+
* safe; // → "My File.txt"
|
|
748
|
+
*
|
|
749
|
+
* // Path traversal ALWAYS throws — never sanitizable.
|
|
750
|
+
* try {
|
|
751
|
+
* b.guardFilename.sanitize("../etc/passwd", { profile: "permissive" });
|
|
752
|
+
* } catch (e) {
|
|
753
|
+
* e.code; // → "filename.traversal"
|
|
754
|
+
* }
|
|
755
|
+
*/
|
|
637
756
|
function sanitize(input, opts) {
|
|
757
|
+
var rawMode = opts && opts.mode;
|
|
638
758
|
opts = _resolveOpts(opts);
|
|
639
759
|
if (typeof input !== "string" && !Buffer.isBuffer(input)) {
|
|
640
760
|
throw _err("filename.bad-input", "sanitize requires string or Buffer input");
|
|
641
761
|
}
|
|
762
|
+
if (rawMode === "strip") {
|
|
763
|
+
var stripped = _sanitizeStripMode(input, opts);
|
|
764
|
+
if (opts.audit && typeof opts.audit.safeEmit === "function") {
|
|
765
|
+
try {
|
|
766
|
+
opts.audit.safeEmit({
|
|
767
|
+
action: "guardfilename.sanitize.stripped",
|
|
768
|
+
outcome: "success",
|
|
769
|
+
metadata: {
|
|
770
|
+
originalLength: Buffer.byteLength(
|
|
771
|
+
typeof input === "string" ? input : input.toString("utf8"), "utf8"),
|
|
772
|
+
sanitizedLength: Buffer.byteLength(stripped, "utf8"),
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
} catch (_e) { /* drop-silent — audit sinks must never crash the producer */ }
|
|
776
|
+
}
|
|
777
|
+
return stripped;
|
|
778
|
+
}
|
|
642
779
|
return _sanitize(input, opts);
|
|
643
780
|
}
|
|
644
781
|
|
|
782
|
+
/**
|
|
783
|
+
* @primitive b.guardFilename.gate
|
|
784
|
+
* @signature b.guardFilename.gate(opts?)
|
|
785
|
+
* @since 0.7.5
|
|
786
|
+
* @status stable
|
|
787
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
788
|
+
* @related b.guardFilename.validate, b.guardFilename.sanitize, b.fileUpload.create
|
|
789
|
+
*
|
|
790
|
+
* Build a `b.gateContract` gate that consumes `ctx.filename` (or
|
|
791
|
+
* `ctx.name`). Action chain: `serve` (no filename or clean) →
|
|
792
|
+
* `audit-only` (warn-only issues) → `sanitize` (critical/high but
|
|
793
|
+
* every reject-policy off — strip-eligible classes only) → `refuse`
|
|
794
|
+
* (any reject-policy active or sanitize fails). Path-traversal /
|
|
795
|
+
* null-byte / NTFS-ADS / UNC / overlong-UTF-8 always cause `refuse`
|
|
796
|
+
* — there is no `sanitize` action for those classes.
|
|
797
|
+
*
|
|
798
|
+
* @opts
|
|
799
|
+
* profile: "strict"|"balanced"|"permissive",
|
|
800
|
+
* compliance: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
801
|
+
* name: string, // gate identity for audit / observability
|
|
802
|
+
*
|
|
803
|
+
* @example
|
|
804
|
+
* var fnGate = b.guardFilename.gate({ profile: "strict" });
|
|
805
|
+
* var verdict = await fnGate.check({ filename: "../etc/passwd" });
|
|
806
|
+
* verdict.action; // → "refuse"
|
|
807
|
+
*
|
|
808
|
+
* var ok = await fnGate.check({ filename: "report.txt" });
|
|
809
|
+
* ok.action; // → "serve"
|
|
810
|
+
*/
|
|
645
811
|
function gate(opts) {
|
|
646
812
|
opts = _resolveOpts(opts);
|
|
647
813
|
return gateContract.buildGuardGate(
|
|
@@ -682,13 +848,79 @@ function gate(opts) {
|
|
|
682
848
|
});
|
|
683
849
|
}
|
|
684
850
|
|
|
851
|
+
/**
|
|
852
|
+
* @primitive b.guardFilename.buildProfile
|
|
853
|
+
* @signature b.guardFilename.buildProfile(opts)
|
|
854
|
+
* @since 0.7.5
|
|
855
|
+
* @status stable
|
|
856
|
+
* @related b.guardFilename.gate, b.guardFilename.compliancePosture
|
|
857
|
+
*
|
|
858
|
+
* Compose a derived profile from one or more named bases plus inline
|
|
859
|
+
* overrides. `opts.extends` is a profile name (`"strict"` /
|
|
860
|
+
* `"balanced"` / `"permissive"`) or an array of names; later entries
|
|
861
|
+
* shadow earlier ones. Inline `opts` keys win last.
|
|
862
|
+
*
|
|
863
|
+
* @opts
|
|
864
|
+
* extends: string|string[], // base profile name(s) to compose
|
|
865
|
+
*
|
|
866
|
+
* @example
|
|
867
|
+
* var custom = b.guardFilename.buildProfile({
|
|
868
|
+
* extends: "balanced",
|
|
869
|
+
* extensionAllowlist: [".pdf", ".png", ".jpg"],
|
|
870
|
+
* });
|
|
871
|
+
* custom.extensionAllowlist.length; // → 3
|
|
872
|
+
* custom.traversalPolicy; // → "reject"
|
|
873
|
+
*/
|
|
685
874
|
var buildProfile = gateContract.makeProfileBuilder(PROFILES);
|
|
686
875
|
|
|
876
|
+
/**
|
|
877
|
+
* @primitive b.guardFilename.compliancePosture
|
|
878
|
+
* @signature b.guardFilename.compliancePosture(name)
|
|
879
|
+
* @since 0.7.5
|
|
880
|
+
* @status stable
|
|
881
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
882
|
+
* @related b.guardFilename.gate, b.guardFilename.buildProfile
|
|
883
|
+
*
|
|
884
|
+
* Look up a compliance-posture overlay by name (`"hipaa"` /
|
|
885
|
+
* `"pci-dss"` / `"gdpr"` / `"soc2"`). Returns the posture object —
|
|
886
|
+
* the caller may mutate freely. Throws
|
|
887
|
+
* `GuardFilenameError("filename.bad-posture")` on unknown name.
|
|
888
|
+
*
|
|
889
|
+
* @example
|
|
890
|
+
* var posture = b.guardFilename.compliancePosture("hipaa");
|
|
891
|
+
* posture.requireAscii; // → true
|
|
892
|
+
* posture.shellExecExtPolicy; // → "reject"
|
|
893
|
+
*/
|
|
687
894
|
function compliancePosture(name) {
|
|
688
895
|
return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES, _err, "filename");
|
|
689
896
|
}
|
|
690
897
|
|
|
691
898
|
var _filenameRulePacks = gateContract.makeRulePackLoader(GuardFilenameError, "filename");
|
|
899
|
+
/**
|
|
900
|
+
* @primitive b.guardFilename.loadRulePack
|
|
901
|
+
* @signature b.guardFilename.loadRulePack(pack)
|
|
902
|
+
* @since 0.7.5
|
|
903
|
+
* @status stable
|
|
904
|
+
* @related b.guardFilename.gate
|
|
905
|
+
*
|
|
906
|
+
* Register an operator-supplied rule pack with the guard-filename
|
|
907
|
+
* registry. The pack is identified by `pack.id` (non-empty string)
|
|
908
|
+
* and stored for later inspection / dispatch by gates that opt in
|
|
909
|
+
* via `opts.rulePackId`. Returns the pack object unchanged on
|
|
910
|
+
* success; throws `GuardFilenameError("filename.bad-opt")` when
|
|
911
|
+
* `pack` is missing or `pack.id` is not a non-empty string.
|
|
912
|
+
*
|
|
913
|
+
* @example
|
|
914
|
+
* var pack = b.guardFilename.loadRulePack({
|
|
915
|
+
* id: "tenant-uploads-policy",
|
|
916
|
+
* rules: [
|
|
917
|
+
* { id: "tenant-prefix", severity: "high",
|
|
918
|
+
* detect: function (n) { return n.indexOf("tenant_") !== 0; },
|
|
919
|
+
* reason: "tenant policy: filenames must be tenant_-prefixed" },
|
|
920
|
+
* ],
|
|
921
|
+
* });
|
|
922
|
+
* pack.id; // → "tenant-uploads-policy"
|
|
923
|
+
*/
|
|
692
924
|
var loadRulePack = _filenameRulePacks.load;
|
|
693
925
|
|
|
694
926
|
module.exports = {
|