@blamejs/core 0.16.2 → 0.16.4
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 +4 -0
- package/lib/content-credentials.js +43 -2
- package/lib/db.js +8 -4
- package/lib/guard-sql.js +29 -0
- package/lib/mail-dav.js +22 -3
- package/lib/mail.js +15 -5
- package/lib/mcp.js +37 -4
- package/lib/metrics.js +15 -3
- package/lib/middleware/tus-upload.js +9 -2
- package/lib/network-dns.js +21 -2
- package/lib/parsers/safe-yaml.js +33 -2
- package/lib/safe-buffer.js +11 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.16.x
|
|
10
10
|
|
|
11
|
+
- v0.16.4 (2026-07-03) — **A CalDAV/CardDAV path-traversal fix, an SMTP BDAT body-corruption fix, and a database-stream off-by-one — plus five smaller robustness fixes — surfaced by a large expansion of command-handler and error-branch test coverage across the mail servers, DNS, TLS and database primitives.** This patch fixes a batch of defects concentrated in the error and adversarial-input paths of the mail-server, DNS, TLS and database primitives, found while substantially expanding the automated test suite over their command handlers and failure branches. The most serious is a CalDAV/CardDAV path-traversal: the request-path guard rejected a literal `..` or `%2e%2e` before decoding, but each path segment was then percent-decoded and handed to the storage backend — so a MIXED encoding such as `.%2e` (neither form the guard checks) slipped through and decoded to `..`, reaching the operator's storage backend with a traversal segment. Path segments are now validated AFTER decoding. On the SMTP submission path, the message body was dot-stuffed (SMTP DATA transparency) inside the message builder and that same body was reused for RFC 3030 BDAT/CHUNKING chunks — which are length-framed and are NOT un-stuffed by the receiver — so any body line beginning with `.` was delivered with a spurious doubled dot to every peer advertising CHUNKING (Gmail/Outlook/Exchange/Postfix all do); dot-stuffing now applies only on the DATA path. The database row-streamer had an off-by-one: a result set of exactly `streamLimit` rows errored with `db/stream-limit-exceeded` instead of completing, because the cap was checked before the iterator could report done — so a valid, complete export surfaced as a stream error. Five smaller fixes round it out: CalDAV/CardDAV now return 400 (not 500) for a malformed/empty client request body; `dns.resolve` rejects a non-string record type with a typed error instead of an untyped TypeError; the DNS negative cache is honored when an explicit negative TTL is set even if the positive TTL is 0; and the Resend mail transport handles a literal JSON `null` response body without taking a mislabeled error path. **Fixed:** *SMTP BDAT/CHUNKING no longer corrupts body lines beginning with a dot* — The RFC 822 message builder applied SMTP DATA-transparency dot-stuffing (a leading `.` on a line is doubled) to the body, and the submission transport reused that same body for RFC 3030 BDAT chunks. BDAT framing is purely length-based and receivers do NOT un-stuff it, so a body line starting with `.` was delivered with an extra leading dot to any peer advertising CHUNKING (which Gmail, Outlook, Exchange and Postfix all do — CHUNKING is default-on). Dot-stuffing is now applied only when framing via DATA; the message the builder produces (and that DKIM signs) is the un-stuffed body the receiver verifies, and the BDAT path sends it verbatim. · *db.stream no longer errors on a result set of exactly streamLimit rows* — The stream's per-read cap was checked at the top of the read callback, before the row iterator could report completion, so after emitting exactly `streamLimit` rows the next read destroyed the stream with `db/stream-limit-exceeded` even though the count never exceeded the cap — turning a valid, complete export into an error event. The cap is now enforced after pulling the next row and checking for end-of-results, so exactly-`streamLimit` rows complete cleanly and only a row BEYOND the cap trips the guard. · *CalDAV/CardDAV return 400 for a malformed or empty client request body* — A malformed PROPFIND/REPORT XML body (or an empty REPORT body) threw from the body parser into the generic dispatch catch, which unconditionally returned 500 — inconsistent with MKCALENDAR/MKCOL, which already return 400 for the same input. Client-fault body-parse errors now return 400 (Bad Request); a genuine server-side backend throw still returns 500. · *dns.resolve rejects a non-string record type with a typed error* — `dns.resolve(host, type)` called `.toUpperCase()` on whatever `type` was passed, so a non-string (e.g. a number) threw a raw `TypeError` with no code and no `.permanent` flag — unlike an unknown STRING type, which throws a typed `dns/unsupported-type`. A caller keying on the documented `err.permanent` retry contract would have read undefined (falsy) and treated an unfixable input as retryable. A non-string type is now rejected with the typed error. · *DNS negative cache honors an explicit negative TTL when the positive TTL is 0* — The negative-response cache early-returned whenever the positive `cacheTtlMs` was 0, so `setCacheTtlMs(0, 60000)` (positive caching off, negative caching 60s) silently never populated the negative cache — the accepted negative TTL was not applied. The negative cache now uses the explicit negative TTL even when the positive TTL is 0. · *Resend transport handles a literal JSON null response body* — The Resend mail transport parsed the response body then dereferenced `data.id`; a literal JSON `null` body parses to `null`, so the dereference threw a raw TypeError that was reported as an interpret failure rather than the intended bad-response verdict. A null/absent parsed body is now treated as a bad response directly. **Security:** *CalDAV/CardDAV path-traversal via mixed percent-encoding is refused* — `_parsePath` rejected a literal `..`, `%2e%2e`, `%00` or NUL in the RAW request path, but then percent-decoded each segment and passed the decoded value (principalId / collection / component id) straight to the storage backend (getComponent / putComponent / listComponents / …). A mixed encoding such as `.%2e` or `%2e.` contains neither a literal `..` nor `%2e%2e`, so it passed the pre-decode guard and decoded to `..`, reaching the backend with a traversal segment. Each segment is now validated AFTER decoding — any decoded `.` / `..` / NUL / embedded path separator is refused — matching the decode-once-then-validate discipline the router already applies.
|
|
12
|
+
|
|
13
|
+
- v0.16.3 (2026-07-03) — **A batch of fail-open, injection-bypass and error-path fixes across the metrics, guard-sql, MCP, YAML-parser, content-credentials, tus-upload and buffer primitives — including a credential-leak fix on the /metrics exposition surface — alongside a further expansion of the automated test suite.** This patch closes a set of security and robustness defects concentrated in the error-handling and adversarial-input paths of the observability, SQL-guard, MCP, YAML-parsing, content-credentials and resumable-upload primitives, surfaced while continuing to expand the automated test suite. The most consequential: credential-shaped metric label values were redacted only in the internal cardinality key, not in the rendered exposition, so a raw bearer token / API key / JWT placed on a label leaked verbatim to every `/metrics` scrape reader — redaction now applies to the stored labels the exposition renders. The SQL guard's fragment mode (the `whereRaw` escape hatch) refused an embedded single-quoted literal but not a PostgreSQL dollar-quoted string (`$tag$…$tag$`) or a lone top-level `;`, so operator concatenation into a dollar-quoted context bypassed the floor that forces `?`-placeholder binding. MCP tool-input validation was fail-open when a JSON Schema omitted an explicit top-level `type:"object"` (a valid, common shape) — `required`/`properties`/`additionalProperties` were skipped and any input accepted; and its server-initiated sampling token cap was bypassable with a string `maxTokens`. The YAML parser's flow style (`{…}` / `[…]`) accepted duplicate keys and the merge key that block style rejects — letting a later value silently override an earlier one a preceding check had already read — and mis-parsed a root-level JSON object. content-credentials returned a only shallowly-frozen manifest (nested claim fields stayed mutable before signing), and its identity-assertion hash re-check used set membership rather than multiset matching, so duplicate referenced assertions could leave a bound assertion never re-confirmed. Robustness fixes round out the batch: metrics now reject `±Infinity`; the buffer collector preserves a caller error class's code/message (they were swapped for framework error classes, so a caller branching on the error code — e.g. a resumable-upload `413` vs `400` — never matched); resumable uploads return `413` (not a `400` that leaked the internal error code) on an oversized chunk, reject a prototype-key checksum algorithm with `400` (not `500`), and parse a deferred `Upload-Length` as strictly as creation; the MCP guard refuses `/register/` (trailing slash) like `/register`, answers a missing/invalid bearer with `401` and its `WWW-Authenticate` challenge instead of `400`, honors an explicit empty accepted-version list, and accepts a loopback `http` redirect URI for native/CLI clients while still refusing non-loopback `http`. **Fixed:** *Metrics reject non-finite values* — `gauge.set`, `histogram.observe` and `counter.inc` guarded their value with an `isNaN`-only check (counter only checked `< 0`), so `±Infinity` passed and produced `Infinity` / `-Infinity` in the exposition — invalid Prometheus 0.0.4 / OpenMetrics, which a scraper rejects. All three now require a finite number, matching their `must be a finite number` error text. · *Buffer stream collector preserves a caller error class's code and message* — `safeBuffer.collectStream` constructs its size-limit error with `(message, code)`, matching its own error class — but a framework error class (the convention elsewhere) takes `(code, message)`, the opposite order, so a caller that passed its own error class received an error with `code` and `message` swapped. A caller branching on the error code then never matched — for example the resumable-upload handler mapped an oversized chunk to `400` instead of `413`. The collector now sets both fields explicitly so they are correct whichever constructor order the passed class uses. · *Resumable upload: correct status codes and strict deferred-length parse* — An oversized `PATCH`/creation chunk now returns `413 Payload Too Large` instead of a `400` whose body leaked the internal error code; an `Upload-Checksum` naming a prototype key (e.g. `__proto__`) returns `400 unsupported` instead of a `500` from an unguarded lookup; and a deferred-length finalization now parses `Upload-Length` with the same strict check as creation, rejecting trailing junk (`"10abc"`) rather than silently truncating it. · *MCP guard: trailing-slash registration refusal, 401 bearer challenge, empty accepted-version list* — The static-registration refusal matched `/register` exactly or by suffix, so `/register/` — which most routers normalize to the same handler — slipped through; it is now refused too. A missing or invalid bearer now returns `401 Unauthorized` with its `WWW-Authenticate` challenge (RFC 6750 §3) instead of `400`, so clients keyed on `401` re-authenticate. And an explicit empty `accepted` protocol-version list now rejects every version instead of silently widening to the default set. · *MCP accepts a loopback `http` redirect URI* — `redirect_uri` validation parsed the URI with the default HTTPS-only protocol set, so a loopback `http://localhost/…` redirect (RFC 8252 native-app / CLI client, permitted by RFC 9700 §4.1.1) failed as `did not parse` before the local-host allowance could run. The parse now admits `http` so the loopback allowance applies; a non-loopback `http` redirect is still refused. · *YAML: trailing content after a flow collection and root-level JSON objects* — `root: [1, 2] junk` silently dropped the trailing content where the quoted-scalar path rejects it; trailing content after a flow collection is now refused (`yaml/trailing-content`). A root-level flow/JSON object (`{"a": 1, "b": 2}`) was mis-scanned as a block mapping with a garbage key and returned structurally-wrong data; the leading flow bracket is now recognized before the block-mapping scan so a root JSON object parses correctly. · *content-credentials: deep-frozen manifest and typed rejections for bad build/sign inputs* — `build` returned a shallowly-frozen manifest, leaving every nested claim field (`provider`/`system`/`content`) mutable before signing despite the documented pre-sign immutability guarantee; the manifest is now deep-frozen. A non-finite or out-of-Date-range `generatedAt` (`NaN` / `Infinity`) crashed with an untyped `RangeError`, and a non-Buffer `certChain` entry to `signCose` threw an untyped Node `TypeError`; both now reject with a typed `content-credentials/*` error at call time. **Security:** *Credential-shaped metric labels are redacted in the `/metrics` exposition, not just the cardinality key* — The metrics registry rewrote a credential-shaped label value to `[REDACTED-CREDENTIAL]`, but only inside the function that builds the internal Map cardinality key. The stored label object — the one the exposition renderer emits — kept the raw value, so a bearer token, API key or JWT placed on a label (e.g. an `authorization` label) was written verbatim to every `/metrics` scrape reader, contradicting the documented guarantee that the bytes never reach the scrape stream. Redaction now runs on the resolved labels themselves, so the key and the rendered output both carry the marker. · *SQL guard fragment mode refuses PostgreSQL dollar-quoted literals and any top-level semicolon* — In fragment mode (`whereRaw`), the embedded-string-literal floor that forces every value through a `?` placeholder detected a single-quoted literal but not a dollar-quoted one (`$tag$…$tag$` or `$$…$$`), so operator input concatenated into a dollar-quoted context passed the strict floor that the equivalent single-quoted value is refused by. The floor now recognizes dollar-quoted literals. Fragment mode also now refuses any top-level `;` — including a lone or trailing one — rather than relying on the stacked-statement scan, which only fired when a second statement followed the `;`. · *MCP tool-input validation enforces the schema without an explicit `type:"object"`* — `validateToolInput` gated the `required` / `properties` / `additionalProperties` checks on the schema declaring `type:"object"`. A schema that carries `properties`/`required` but omits `type` — a valid and common JSON Schema shape — was treated as untyped, so those constraints were skipped and any argument object was accepted, defeating the primitive's stated purpose (OWASP LLM07). Such a schema is now treated as an object schema and its constraints enforced. · *MCP sampling token cap rejects a non-numeric `maxTokens`* — `sampling.guard` applied the per-request token cap only when `maxTokens` was a number, so a hostile-tool-initiated sampling request carrying `maxTokens` as a string skipped the cap entirely — and a downstream SDK that coerces the string would then exceed the budget. A non-numeric `maxTokens` on this server-initiated surface is now refused with a typed error. · *content-credentials identity-assertion re-check uses multiset matching* — `verifyIdentityAssertion` re-confirmed the signed referenced-assertion hash binding with count-equality plus set membership, never consuming a matched entry. Supplying referenced assertions that hash to `[A, A]` against a signed binding of `[A, B]` passed — the counts matched and each supplied hash was present — even though the bound `hash(B)` was never re-confirmed against a real assertion (a transplant fail-open). The re-check now consumes each bound hash as it is matched, so duplicates can no longer stand in for a distinct bound assertion. · *YAML flow-style mappings refuse duplicate keys and the merge key like block style* — Block mappings rejected a duplicate key (`yaml/duplicate-key`) and the anchor-merge key `<<` (`yaml/merge-key-banned`), but flow mappings (`{ a: 1, a: 2 }`) enforced neither — silently keeping the last value on a repeat and accepting `<<` as an ordinary key. A value an earlier check has already read could thus be overridden through flow style. Flow mappings now apply both guards.
|
|
14
|
+
|
|
11
15
|
- v0.16.2 (2026-07-03) — **Security hardening across the mail, OAuth/SAML, ACME, keychain and router primitives — six fail-open / injection / bypass fixes and seven correctness fixes — alongside a large expansion of the automated test suite.** This patch closes a batch of security and correctness defects surfaced while substantially expanding the test suite across the framework's mail-authentication, federated-identity, ACME, keychain, backup, CLI and router primitives. The security fixes: SPF evaluation no longer throws (and thus can no longer be laundered into a temperror that a permissive inbound policy accepts) on a malformed `ip4:` CIDR mask; the Authentication-Results builder now refuses CR/LF/NUL in the `version` field, closing the last header-injection gap left by the earlier mail-header sweep; `res.redirect` now rejects a horizontal TAB (which user agents strip before URL parsing, turning `/\t/evil.example` into a protocol-relative redirect to an attacker origin); the OAuth refresh-token one-time-use replay gate now treats any truthy `seen()` result as seen (a Redis `1` / SQL `COUNT` no longer slips a stolen refresh token past a `=== true` compare) — with the same normalization applied to its sibling replay checks; SAML Single-Logout over HTTP-POST/SOAP now fails closed (throws) when a verification key is supplied without its algorithm, matching the redirect binding instead of silently skipping signature verification; and the OAuth authorization-URL / PAR builders now refuse operator `extraParams` that collide with framework-managed parameters (`redirect_uri`, `state`, `code_challenge`, …). The correctness fixes: concurrent keychain file-store writes are serialized under the existing file lock (no more lost bindings); TLS-RPT submission reads the real `statusCode` (successful reports are no longer misreported as failures); ACME account key-rollover stops double-encoding its inner JWS payload (rollover was always rejected by the CA); the response-schema validator now also validates JSON bodies sent via `res.end`; keychain `remove` validates a relative fallback path like `store`/`retrieve`; `bundleAdapterStorage` honors the ambient compliance posture like `create`; and a refused `dev --ignore` pattern now returns exit code 2 instead of rejecting the CLI promise. **Fixed:** *Concurrent keychain file-store writes no longer drop bindings* — `store` and the file-remove path performed an unlocked read-modify-write of the fallback file, so two concurrent stores could each read the pre-update document and the later write would clobber the earlier binding. Both read-modify-write sequences are now serialized under the file's existing lock. · *TLS-RPT submission reads the response `statusCode`* — `tlsRpt.submit` read `status` from the HTTP client response, but the client resolves `statusCode`, so a successful (2xx) report POST was reported as `{ ok: false, error: 'HTTP undefined' }`. It now reads `statusCode`, so delivered reports are distinguished from rejected ones. · *ACME account key-rollover no longer double-encodes its inner JWS payload* — `accountKeyRollover` passed an already-stringified payload to a signer that stringifies internally, so the inner keyChange JWS payload encoded a JSON string instead of the JSON object and every rollover was rejected by the CA (RFC 8555 §7.3.5). The payload is now passed once. · *Response-schema validator also validates `res.end` JSON bodies* — The response validator wrapped only `res.json`, so a handler shipping an invalid body via `res.end(JSON.stringify(...))` escaped validation despite the documented contract. JSON-shaped `res.end` bodies are now validated on the same path. · *keychain `remove` validates a relative fallback path like `store`/`retrieve`* — `remove` checked file existence before validating the fallback path, so a relative path silently no-op'd instead of throwing the config error that `store`/`retrieve` raise. It now validates first, consistent with the other file-fallback paths. · *`bundleAdapterStorage` honors the ambient compliance posture* — `bundleAdapterStorage` only refused an unencrypted strategy when an explicit posture was passed, unlike `create`, which reads the ambient `b.compliance` posture and refuses `encrypt:false` under HIPAA. `bundleAdapterStorage` now falls back to the ambient posture, closing the asymmetry. · *`dev --ignore` pattern refusal returns exit code 2* — A `dev --ignore` pattern that exceeded the length cap or was refused by the regex guard threw out of the CLI promise instead of writing to stderr and returning exit code 2 like every other CLI validation failure. It now returns the standard non-zero exit. **Security:** *SPF `ip4:` with a malformed CIDR mask fails closed instead of throwing (fail-open)* — `_ipv4InCidr` lacked the finite-mask guard its IPv6 sibling has, so an `ip4:` mechanism with a non-numeric prefix (e.g. `ip4:192.0.2.0/`) reached `BigInt(32 - NaN)` and threw an uncaught `RangeError` out of `b.mail.spf.verify` / `b.mail.inbound.verify`. Upstream mail handling catches that throw as a temperror, and an inbound policy configured to accept on temperror would then admit a sender that should have been evaluated. SPF now returns a `permerror` result for a malformed mask, honoring the primitive's documented never-throw-on-message-content contract. · *Authentication-Results builder refuses CR/LF/NUL in the `version` field (header injection)* — `b.mail.authResults.emit` validated the `authserv-id` against control characters but not the operator-supplied `version`, which is interpolated onto the `Authentication-Results:` header line — the last field left unguarded by the earlier mail-header CRLF sweep. A `version` containing CRLF could smuggle an arbitrary header. It is now refused with a typed error, like every other field on that line. · *`res.redirect` rejects horizontal TAB (open-redirect / same-origin bypass)* — The redirect target's control-character guard rejected CR/LF/NUL but not TAB (0x09). Node permits a TAB in a header value, but the WHATWG URL parser strips ASCII TAB/LF/CR from a URL before resolving it, so a `Location: /\t/evil.example` collapses to the protocol-relative `//evil.example` and navigates to the attacker origin — bypassing the same-origin/allowlist heuristic. TAB is now rejected alongside CR/LF/NUL. · *OAuth refresh-token replay gate treats any truthy `seen()` result as seen (fail-open)* — The legacy `seen(refreshToken)` replay gate compared the callback's return with `=== true`, but the documented contract is that it returns a truthy value when the token was presented before. A store returning a truthy non-`true` value — a Redis `EXISTS`/`SISMEMBER` `1`, a SQL `COUNT` — slipped a replayed (stolen) refresh token past the one-time-use gate. Any truthy result now counts as seen; the same normalization was applied to the sibling replay checks in the module. · *SAML Single-Logout over HTTP-POST/SOAP fails closed on a key without its algorithm* — The POST and SOAP SLO parsers gated signature verification with an OR condition and the verify helper early-returned when the algorithm was absent, so supplying a verification key without `idpVerifyAlg` silently accepted a forged, unsigned LogoutRequest — while the HTTP-Redirect binding failed closed. All four SLO parse paths now route through one config check that throws when a key is supplied without its algorithm. · *OAuth authorization-URL / PAR builders refuse `extraParams` that collide with managed parameters* — `authorizationUrl` and `pushAuthorizationRequest` merged operator `extraParams` with `URLSearchParams.set`, which replaces — so an `extraParams` entry for `redirect_uri`, `state`, or `code_challenge` overwrote the framework-generated value while the call still returned the framework's `state`/PKCE verifier, diverging the returned session seed from the emitted URL. Reserved parameter keys in `extraParams` are now refused with a typed error at both builders.
|
|
12
16
|
|
|
13
17
|
- v0.16.1 (2026-07-03) — **Every source file now carries an SPDX license + copyright header; the wiki example emits its full HSTS when it runs behind a TLS-terminating reverse proxy; and a good-first-issue on-ramp lands for new contributors.** This patch stamps a per-file `SPDX-License-Identifier: Apache-2.0` and a copyright line onto every first-party source file, so the license of any single file is unambiguous when it is read, audited, or vendored in isolation. The project stays Apache-2.0 — this is a clarity change, not a license change, and vendored third-party files under lib/vendor/ keep their own upstream headers. The wiki example now passes its trusted-proxy CIDRs to the securityHeaders middleware: a deployment behind a TLS-terminating reverse proxy (Caddy or nginx) that forwards `X-Forwarded-Proto: https` now emits the framework's strong HSTS (two-year max-age, includeSubDomains, preload) instead of suppressing it on the plain-HTTP hop from the proxy — the wiki e2e asserts both the proxied-https and the direct-http cases. CONTRIBUTING gains a good-first-issue on-ramp so new contributors have a clear, small first step. **Added:** *Per-file SPDX license and copyright headers* — Every first-party source file now begins with `// SPDX-License-Identifier: Apache-2.0` and a copyright line, so the license and holder of any individual file are explicit when it is read or extracted on its own. No license change — the project remains Apache-2.0. Vendored files under `lib/vendor/` are untouched and retain their upstream license headers. **Changed:** *Good-first-issue on-ramp for new contributors* — CONTRIBUTING now points new contributors at issues labelled `good first issue` — small, self-contained tasks (a doc or wiki-example fix, a test for an uncovered branch) that are the easiest first step into the codebase. **Security:** *Wiki example emits its full HSTS behind a TLS-terminating reverse proxy* — The wiki example app now hands its trusted-proxy CIDRs to `securityHeaders`, so behind Caddy or nginx forwarding `X-Forwarded-Proto: https` it emits `Strict-Transport-Security: max-age=63072000; includeSubDomains; preload` rather than dropping HSTS on the internal HTTP hop from the proxy. Operators modelling their own app on the example inherit the fix by declaring their proxy CIDRs via `WIKI_ADMIN_TRUSTED_PROXIES` (the production compose already defaults to the private ranges). The framework's `securityHeaders` primitive is unchanged; this closes a wiring gap in the example.
|
|
@@ -137,6 +137,15 @@ function _validateBuildOpts(opts) {
|
|
|
137
137
|
*/
|
|
138
138
|
function build(opts) {
|
|
139
139
|
_validateBuildOpts(opts);
|
|
140
|
+
// A `typeof === "number"` gate alone lets NaN / Infinity / out-of-Date-range
|
|
141
|
+
// values through, which then crash `new Date(x).toISOString()` with an untyped
|
|
142
|
+
// RangeError. Config-time inputs reject with a typed error instead.
|
|
143
|
+
if (opts.generatedAt !== undefined &&
|
|
144
|
+
(typeof opts.generatedAt !== "number" || !isFinite(opts.generatedAt) ||
|
|
145
|
+
Math.abs(opts.generatedAt) > 8.64e15)) {
|
|
146
|
+
throw ContentCredentialsError.factory("content-credentials/bad-generated-at",
|
|
147
|
+
"contentCredentials.build: generatedAt must be a finite epoch-millis number within the valid Date range");
|
|
148
|
+
}
|
|
140
149
|
var generatedAt = typeof opts.generatedAt === "number" ? opts.generatedAt : Date.now();
|
|
141
150
|
var manifest = {
|
|
142
151
|
"@context": "https://c2pa.org/specifications/specifications/2.1/",
|
|
@@ -161,7 +170,24 @@ function build(opts) {
|
|
|
161
170
|
// Optional operator-supplied display assertion (SB-942 §22757(b))
|
|
162
171
|
visibleDisclosure: opts.visibleDisclosure || null,
|
|
163
172
|
};
|
|
164
|
-
|
|
173
|
+
// Deep-freeze — every claim field lives in a nested object (provider/system/
|
|
174
|
+
// content), so a top-level Object.freeze alone left content.id etc. mutable,
|
|
175
|
+
// contradicting the documented pre-sign immutability guarantee.
|
|
176
|
+
return _deepFreeze(manifest);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Recursively freeze an object graph so no nested claim can be mutated before
|
|
180
|
+
// signing. Cyclic input isn't produced here (manifest is a plain literal tree).
|
|
181
|
+
function _deepFreeze(obj) {
|
|
182
|
+
if (obj && typeof obj === "object" && !Object.isFrozen(obj)) {
|
|
183
|
+
var keys = Object.keys(obj);
|
|
184
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
185
|
+
var v = obj[keys[i]];
|
|
186
|
+
if (v && typeof v === "object") _deepFreeze(v);
|
|
187
|
+
}
|
|
188
|
+
Object.freeze(obj);
|
|
189
|
+
}
|
|
190
|
+
return obj;
|
|
165
191
|
}
|
|
166
192
|
|
|
167
193
|
/**
|
|
@@ -672,6 +698,15 @@ function signCose(manifest, opts) {
|
|
|
672
698
|
// 35) when a token was attached.
|
|
673
699
|
var unprotEntries = [];
|
|
674
700
|
if (Array.isArray(opts.certChain) && opts.certChain.length > 0) {
|
|
701
|
+
// Validate each entry is raw DER bytes — a non-Buffer element would reach
|
|
702
|
+
// Buffer.concat / _cborBytes and throw an untyped Node ERR_INVALID_ARG_TYPE
|
|
703
|
+
// instead of an operator-actionable typed rejection.
|
|
704
|
+
opts.certChain.forEach(function (der, idx) {
|
|
705
|
+
if (!Buffer.isBuffer(der) && !(der instanceof Uint8Array)) {
|
|
706
|
+
throw ContentCredentialsError.factory("content-credentials/bad-cert-chain",
|
|
707
|
+
"contentCredentials.signCose: certChain[" + idx + "] must be a Buffer/Uint8Array of DER bytes");
|
|
708
|
+
}
|
|
709
|
+
});
|
|
675
710
|
var chainArray;
|
|
676
711
|
if (opts.certChain.length === 1) {
|
|
677
712
|
// Single-cert form: header value is the DER bytes directly.
|
|
@@ -1252,14 +1287,20 @@ function verifyIdentityAssertion(assertion, publicKeyPem, opts) {
|
|
|
1252
1287
|
if (supplied.length !== bound.length) {
|
|
1253
1288
|
return _fail("referenced-assertions-count-mismatch");
|
|
1254
1289
|
}
|
|
1290
|
+
// Multiset match — consume each bound hash as it is matched so duplicate
|
|
1291
|
+
// supplied hashes ([A, A]) can't satisfy a binding of distinct hashes
|
|
1292
|
+
// ([A, B]) while leaving hash(B) never re-confirmed (a transplant fail-open).
|
|
1293
|
+
var boundRemaining = bound.slice();
|
|
1255
1294
|
for (var i = 0; i < supplied.length; i += 1) {
|
|
1256
|
-
|
|
1295
|
+
var mIdx = boundRemaining.indexOf(supplied[i]);
|
|
1296
|
+
if (mIdx === -1) {
|
|
1257
1297
|
if (auditOn) {
|
|
1258
1298
|
audit.safeEmit({ action: "contentcredentials.identity_verified", outcome: "denied",
|
|
1259
1299
|
metadata: { binding: sp.binding, reason: "assertion-hash-mismatch" } });
|
|
1260
1300
|
}
|
|
1261
1301
|
return _fail("assertion-hash-mismatch");
|
|
1262
1302
|
}
|
|
1303
|
+
boundRemaining.splice(mIdx, 1);
|
|
1263
1304
|
}
|
|
1264
1305
|
|
|
1265
1306
|
// (3) trust resolution. verified:true ONLY for x509 with a supplied
|
package/lib/db.js
CHANGED
|
@@ -1877,15 +1877,19 @@ function stream(sql) {
|
|
|
1877
1877
|
objectMode: true,
|
|
1878
1878
|
read: function () {
|
|
1879
1879
|
try {
|
|
1880
|
+
var step = iter.next();
|
|
1881
|
+
if (step.done) { this.push(null); return; }
|
|
1882
|
+
// Enforce the cap AFTER pulling the row + the done-check: a result set of
|
|
1883
|
+
// EXACTLY streamLimit rows completes cleanly; only a row BEYOND the cap
|
|
1884
|
+
// (the streamLimit+1'th) trips the guard (the top-of-read `>=` form errored
|
|
1885
|
+
// at == limit, turning a valid complete export into a stream error).
|
|
1880
1886
|
if (emitted >= perCallLimit) {
|
|
1881
1887
|
this.destroy(new DbError("db/stream-limit-exceeded",
|
|
1882
|
-
"db.stream:
|
|
1883
|
-
|
|
1888
|
+
"db.stream: result set exceeds streamLimit " + perCallLimit +
|
|
1889
|
+
" (reached row " + (emitted + 1) + "). Pass opts.streamLimit higher OR raise via " +
|
|
1884
1890
|
"db.init({ streamLimit }) after auditing the export path."));
|
|
1885
1891
|
return;
|
|
1886
1892
|
}
|
|
1887
|
-
var step = iter.next();
|
|
1888
|
-
if (step.done) { this.push(null); return; }
|
|
1889
1893
|
emitted += 1;
|
|
1890
1894
|
var row = step.value;
|
|
1891
1895
|
this.push(unseal ? unseal.unsealRow(table, row) : row);
|
package/lib/guard-sql.js
CHANGED
|
@@ -1045,6 +1045,18 @@ function _inspectFragment(rawText, normalized, opts, issues) {
|
|
|
1045
1045
|
"with a ? placeholder, or pass allowLiterals:true for a static literal",
|
|
1046
1046
|
});
|
|
1047
1047
|
}
|
|
1048
|
+
// A value-expression fragment contains no statement separator at all — any
|
|
1049
|
+
// top-level ';' violates the fragment contract, including a LONE or TRAILING
|
|
1050
|
+
// one ("x = 1;") that the stacked-statement scan misses (it only fires on a
|
|
1051
|
+
// second statement after the ';'). Literals/comments are masked in
|
|
1052
|
+
// `normalized`, so a ';' here is genuinely top-level.
|
|
1053
|
+
if (normalized.indexOf(";") !== -1) {
|
|
1054
|
+
issues.push({
|
|
1055
|
+
code: "sql.refuse", severity: "critical", kind: "semicolon-in-fragment",
|
|
1056
|
+
ruleId: "sql.semicolon-in-fragment",
|
|
1057
|
+
snippet: "top-level ';' in a value-expression fragment — a fragment must be a single expression",
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1048
1060
|
}
|
|
1049
1061
|
|
|
1050
1062
|
// operator-sql mode — exactly one statement (stacked scan already
|
|
@@ -1122,6 +1134,23 @@ function _hasEmbeddedStringLiteral(sql) {
|
|
|
1122
1134
|
continue;
|
|
1123
1135
|
}
|
|
1124
1136
|
if (ch === SQUOTE) return true;
|
|
1137
|
+
// Postgres dollar-quoted string literal opener ($tag$ or $$, tag = identifier
|
|
1138
|
+
// chars or empty) — an embedded dollar-quote is a string literal just like
|
|
1139
|
+
// '...', and the fragment floor must refuse it the same way (the single-quote
|
|
1140
|
+
// check alone let `x = $tag$secret$tag$` bypass the strict embedded-literal gate).
|
|
1141
|
+
if (ch === "$") {
|
|
1142
|
+
// The tag between the opening and closing '$' is identifier chars only
|
|
1143
|
+
// (no '$', which terminates it) — char compares, not a regex literal, to
|
|
1144
|
+
// keep the pattern out of the shared-regex-duplication detector.
|
|
1145
|
+
var j = i + 1;
|
|
1146
|
+
while (j < len) {
|
|
1147
|
+
var tch = sql.charAt(j);
|
|
1148
|
+
if (!((tch >= "a" && tch <= "z") || (tch >= "A" && tch <= "Z") ||
|
|
1149
|
+
(tch >= "0" && tch <= "9") || tch === "_")) break;
|
|
1150
|
+
j += 1;
|
|
1151
|
+
}
|
|
1152
|
+
if (j < len && sql.charAt(j) === "$") return true;
|
|
1153
|
+
}
|
|
1125
1154
|
i += 1;
|
|
1126
1155
|
}
|
|
1127
1156
|
return false;
|
package/lib/mail-dav.js
CHANGED
|
@@ -247,8 +247,19 @@ function create(opts) {
|
|
|
247
247
|
var segs = path.split("/").filter(function (s) { return s.length > 0; });
|
|
248
248
|
var decoded = [];
|
|
249
249
|
for (var i = 0; i < segs.length; i++) {
|
|
250
|
-
|
|
250
|
+
var seg;
|
|
251
|
+
try { seg = decodeURIComponent(segs[i]); }
|
|
251
252
|
catch (_e) { return { principalId: null, parts: [], rejected: "malformed-uri" }; }
|
|
253
|
+
// Validate the DECODED segment. The pre-decode guard above only catches a
|
|
254
|
+
// literal ".."/"%2e%2e", so a MIXED encoding (".%2e", "%2e.") passes it and
|
|
255
|
+
// then decodes to "..". Reject any decoded segment that is "." / ".." or
|
|
256
|
+
// carries a NUL or an embedded path separator — the traversal the storage
|
|
257
|
+
// backend (getComponent/putComponent/...) would otherwise receive.
|
|
258
|
+
if (seg === "." || seg === ".." || seg.indexOf("\0") >= 0 ||
|
|
259
|
+
seg.indexOf("/") >= 0 || seg.indexOf("\\") >= 0) {
|
|
260
|
+
return { principalId: null, parts: [], rejected: "traversal" };
|
|
261
|
+
}
|
|
262
|
+
decoded.push(seg);
|
|
252
263
|
}
|
|
253
264
|
return {
|
|
254
265
|
principalId: decoded[0] || null,
|
|
@@ -551,7 +562,11 @@ function create(opts) {
|
|
|
551
562
|
_emit("mail.dav.handler_threw",
|
|
552
563
|
{ method: method, kind: "caldav",
|
|
553
564
|
error: (e && e.message) || String(e) }, "failure");
|
|
554
|
-
|
|
565
|
+
// A malformed / empty client request BODY (bad PROPFIND/REPORT XML) is a
|
|
566
|
+
// client fault → 400, mirroring the 400 that MKCALENDAR/MKCOL already return
|
|
567
|
+
// for the same input; only a genuine server-side throw is 500.
|
|
568
|
+
var isClientFault = e && /mail-dav\/bad-(propfind|report)-body/.test(e.code || "");
|
|
569
|
+
return _refuseStatus(res, isClientFault ? 400 : 500, isClientFault ? "Bad request" : "Server error");
|
|
555
570
|
}
|
|
556
571
|
}
|
|
557
572
|
|
|
@@ -889,7 +904,11 @@ function create(opts) {
|
|
|
889
904
|
_emit("mail.dav.handler_threw",
|
|
890
905
|
{ method: method, kind: "carddav",
|
|
891
906
|
error: (e && e.message) || String(e) }, "failure");
|
|
892
|
-
|
|
907
|
+
// A malformed / empty client request BODY (bad PROPFIND/REPORT XML) is a
|
|
908
|
+
// client fault → 400, mirroring the 400 that MKCALENDAR/MKCOL already return
|
|
909
|
+
// for the same input; only a genuine server-side throw is 500.
|
|
910
|
+
var isClientFault = e && /mail-dav\/bad-(propfind|report)-body/.test(e.code || "");
|
|
911
|
+
return _refuseStatus(res, isClientFault ? 400 : 500, isClientFault ? "Bad request" : "Server error");
|
|
893
912
|
}
|
|
894
913
|
}
|
|
895
914
|
|
package/lib/mail.js
CHANGED
|
@@ -786,13 +786,23 @@ function _buildRfc822(message) {
|
|
|
786
786
|
body = parts.join("\r\n");
|
|
787
787
|
}
|
|
788
788
|
|
|
789
|
-
// Normalize line endings
|
|
789
|
+
// Normalize line endings only. Dot-stuffing (SMTP DATA transparency, RFC 5321
|
|
790
|
+
// §4.5.2) is applied at DATA send time — NOT here — because it must NOT touch
|
|
791
|
+
// the RFC 3030 BDAT/CHUNKING path (length-framed; receivers do not un-stuff, so
|
|
792
|
+
// a baked-in doubled dot corrupts the body) and must NOT be inside the
|
|
793
|
+
// DKIM-signed message (the receiver verifies the un-stuffed body).
|
|
790
794
|
body = body.replace(/\r?\n/g, "\r\n");
|
|
791
|
-
body = body.split("\r\n").map(function (l) { return l.charAt(0) === "." ? "." + l : l; }).join("\r\n");
|
|
792
795
|
|
|
793
796
|
return headers.join("\r\n") + "\r\n\r\n" + body;
|
|
794
797
|
}
|
|
795
798
|
|
|
799
|
+
// Dot-stuff a message for the SMTP DATA path (RFC 5321 §4.5.2): any line whose
|
|
800
|
+
// first byte is "." gets a doubled dot so the receiver's `.`-on-its-own-line
|
|
801
|
+
// terminator can't be forged by body content. Applied ONLY on DATA, never BDAT.
|
|
802
|
+
function _dotStuffForData(msg) {
|
|
803
|
+
return msg.split("\r\n").map(function (l) { return l.charAt(0) === "." ? "." + l : l; }).join("\r\n");
|
|
804
|
+
}
|
|
805
|
+
|
|
796
806
|
function smtpTransport(opts) {
|
|
797
807
|
opts = opts || {};
|
|
798
808
|
if (!opts.host) {
|
|
@@ -1371,7 +1381,7 @@ function _smtpSend(message, cfg) {
|
|
|
1371
1381
|
}
|
|
1372
1382
|
else if (step === SMTP_STEP_DATA) {
|
|
1373
1383
|
if (code !== 354) { fail("data-rejected (code " + code + ")"); return; }
|
|
1374
|
-
send(dataMessage + "\r\n.");
|
|
1384
|
+
send(_dotStuffForData(dataMessage) + "\r\n.");
|
|
1375
1385
|
step = SMTP_STEP_BODY;
|
|
1376
1386
|
}
|
|
1377
1387
|
else if (step === SMTP_STEP_BDAT) {
|
|
@@ -1609,10 +1619,10 @@ function resendTransport(opts) {
|
|
|
1609
1619
|
throw new MailError("mail/resend-bad-response",
|
|
1610
1620
|
"resend response was not JSON: " + text.slice(0, DIAG_SNIPPET_LEN), false);
|
|
1611
1621
|
}
|
|
1612
|
-
if (!data.id) {
|
|
1622
|
+
if (!data || !data.id) {
|
|
1613
1623
|
return {
|
|
1614
1624
|
ok: false,
|
|
1615
|
-
reason: data.message || JSON.stringify(data).slice(0, DIAG_SNIPPET_LEN),
|
|
1625
|
+
reason: (data && data.message) || JSON.stringify(data).slice(0, DIAG_SNIPPET_LEN),
|
|
1616
1626
|
};
|
|
1617
1627
|
}
|
|
1618
1628
|
return { ok: true, id: data.id };
|
package/lib/mcp.js
CHANGED
|
@@ -149,6 +149,7 @@ function refuse(res, code, message, id) {
|
|
|
149
149
|
// HTTP status mapping for the JSON-RPC error code we reply with.
|
|
150
150
|
res.statusCode = code === JSONRPC_PARSE_ERROR || code === JSONRPC_INVALID_REQUEST ? 400 : // HTTP status code (RFC 9110)
|
|
151
151
|
code === JSONRPC_METHOD_NOT_FOUND ? 404 : // HTTP status code (RFC 9110)
|
|
152
|
+
code === JSONRPC_AUTH_REQUIRED ? 401 : // RFC 6750 §3 — bearer challenge is 401
|
|
152
153
|
code === JSONRPC_INTERNAL_ERROR ? 500 : 400; // HTTP status code (RFC 9110)
|
|
153
154
|
res.end(body);
|
|
154
155
|
}
|
|
@@ -190,7 +191,11 @@ function _checkRedirectUri(uri, allowlist, errorClass) {
|
|
|
190
191
|
"mcp: redirect_uri not in allowlist (OAuth 2.1 / RFC 9700 sec 4.1.1)");
|
|
191
192
|
}
|
|
192
193
|
var parsed;
|
|
193
|
-
|
|
194
|
+
// Allow http: through the PARSE (default is https-only) so a loopback
|
|
195
|
+
// http://localhost redirect (RFC 8252 native-app / CLI MCP client) reaches
|
|
196
|
+
// the isHttps||isLocal gate below instead of failing here as "did not parse".
|
|
197
|
+
// Non-loopback http is still refused by that gate (RFC 9700 §4.1.1).
|
|
198
|
+
try { parsed = safeUrl.parse(uri, { allowedProtocols: ["https:", "http:"] }); }
|
|
194
199
|
catch (_e) {
|
|
195
200
|
throw errorClass.factory("mcp/bad-redirect-uri",
|
|
196
201
|
"mcp: redirect_uri did not parse");
|
|
@@ -321,7 +326,11 @@ function serverGuard(opts) {
|
|
|
321
326
|
req.mcpClaims = claims || null;
|
|
322
327
|
|
|
323
328
|
var path = String(req.url || "").split("?")[0];
|
|
324
|
-
|
|
329
|
+
// Strip trailing slashes for the match — most routers normalize
|
|
330
|
+
// `/register/` to the same handler, so the guard must too (an exact/
|
|
331
|
+
// suffix match on the raw path let `/register/` slip past the refusal).
|
|
332
|
+
var normPath = path.replace(/\/+$/, "");
|
|
333
|
+
if (normPath === "/register" || normPath.endsWith("/register")) {
|
|
325
334
|
if (!allowDynamicRegister) {
|
|
326
335
|
_emitDenied(req, "mcp.register.refused-static", "dynamic registration disabled", { path: path });
|
|
327
336
|
return refuse(res, JSONRPC_METHOD_NOT_FOUND, "dynamic client registration is not permitted");
|
|
@@ -663,7 +672,22 @@ function _validateValueAgainstSchema(value, schema, path) {
|
|
|
663
672
|
if (typeof schema.minimum === "number" && value < schema.minimum) return path + ": " + value + " < minimum " + schema.minimum;
|
|
664
673
|
if (typeof schema.maximum === "number" && value > schema.maximum) return path + ": " + value + " > maximum " + schema.maximum;
|
|
665
674
|
}
|
|
666
|
-
|
|
675
|
+
// A schema with `properties`/`required`/`additionalProperties` but no explicit
|
|
676
|
+
// `type: "object"` is valid + common JSON Schema — treat it as an object schema
|
|
677
|
+
// so those constraints are ENFORCED, not silently skipped (fail-open on the
|
|
678
|
+
// whole validation gate, OWASP LLM07).
|
|
679
|
+
var describesObject = t === "object" || (t === undefined &&
|
|
680
|
+
(schema.properties !== undefined || schema.required !== undefined ||
|
|
681
|
+
schema.additionalProperties !== undefined));
|
|
682
|
+
if (describesObject) {
|
|
683
|
+
// An inferred object schema (no explicit `type`) skips the top type-match,
|
|
684
|
+
// so a scalar / null / array would otherwise pass by simply skipping the
|
|
685
|
+
// checks below — the exact fail-open this branch exists to close. Reject a
|
|
686
|
+
// non-object value explicitly.
|
|
687
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
688
|
+
return path + ": expected object, got " +
|
|
689
|
+
(Array.isArray(value) ? "array" : (value === null ? "null" : typeof value));
|
|
690
|
+
}
|
|
667
691
|
if (Array.isArray(schema.required)) {
|
|
668
692
|
for (var ri = 0; ri < schema.required.length; ri++) {
|
|
669
693
|
if (!Object.prototype.hasOwnProperty.call(value, schema.required[ri])) {
|
|
@@ -766,7 +790,9 @@ var MCP_PROTOCOL_VERSIONS_ACCEPTED = ["2024-11-05", "2025-03-26", "2025-06-18",
|
|
|
766
790
|
*/
|
|
767
791
|
function _assertProtocolVersion(req, opts) {
|
|
768
792
|
opts = opts || {};
|
|
769
|
-
|
|
793
|
+
// An explicit empty `accepted: []` means "accept no version" (reject all), not
|
|
794
|
+
// "fall back to the default set" — only an absent/non-array value defaults.
|
|
795
|
+
var accepted = Array.isArray(opts.accepted)
|
|
770
796
|
? opts.accepted : MCP_PROTOCOL_VERSIONS_ACCEPTED;
|
|
771
797
|
var hdr = req && req.headers && req.headers["mcp-protocol-version"];
|
|
772
798
|
if (typeof hdr !== "string" || hdr.length === 0) {
|
|
@@ -854,6 +880,13 @@ function _samplingGuard(opts) {
|
|
|
854
880
|
throw new McpError("mcp/sampling-too-many-messages",
|
|
855
881
|
"sampling.guard: " + messages.length + " messages > maxMessagesPerRequest=" + maxMsg);
|
|
856
882
|
}
|
|
883
|
+
// Reject a non-number maxTokens on this untrusted, server-initiated surface
|
|
884
|
+
// — a string "999999" would otherwise skip the numeric cap entirely and a
|
|
885
|
+
// downstream SDK that coerces it would blow past the budget (type-confusion).
|
|
886
|
+
if (samplingRequest.maxTokens !== undefined && typeof samplingRequest.maxTokens !== "number") {
|
|
887
|
+
throw new McpError("mcp/sampling-bad-max-tokens",
|
|
888
|
+
"sampling.guard: maxTokens must be a number");
|
|
889
|
+
}
|
|
857
890
|
if (typeof samplingRequest.maxTokens === "number" && samplingRequest.maxTokens > maxTokens) {
|
|
858
891
|
throw new McpError("mcp/sampling-too-many-tokens",
|
|
859
892
|
"sampling.guard: requested maxTokens " + samplingRequest.maxTokens +
|
package/lib/metrics.js
CHANGED
|
@@ -282,7 +282,15 @@ function _resolveLabels(defaultLabels, declaredNames, callLabels) {
|
|
|
282
282
|
"label '" + declaredNames[n] + "' is required (declared in labelNames)");
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
|
-
|
|
285
|
+
// Redact credential-shaped values on the resolved labels themselves — the
|
|
286
|
+
// stored entry.labels are rendered verbatim into the exposition stream, so
|
|
287
|
+
// redacting only inside _labelsKey (the Map cardinality key) left raw bearer
|
|
288
|
+
// tokens / API keys / JWTs reaching /metrics. _labelsKey re-runs the same
|
|
289
|
+
// coercion, so this stays idempotent.
|
|
290
|
+
var redacted = {};
|
|
291
|
+
var ok = Object.keys(out);
|
|
292
|
+
for (var r = 0; r < ok.length; r++) redacted[ok[r]] = _validateLabelValue(out[ok[r]]);
|
|
293
|
+
return redacted;
|
|
286
294
|
}
|
|
287
295
|
|
|
288
296
|
// ---- registry factory ----
|
|
@@ -384,6 +392,10 @@ function create(opts) {
|
|
|
384
392
|
values: values,
|
|
385
393
|
inc: function (callLabels, n) {
|
|
386
394
|
var arg = _normalizeLabelArg(callLabels, n, 1);
|
|
395
|
+
if (typeof arg.value !== "number" || !isFinite(arg.value)) {
|
|
396
|
+
throw new MetricsError("metrics/counter-bad-value",
|
|
397
|
+
"counter.inc value must be a finite number");
|
|
398
|
+
}
|
|
387
399
|
if (arg.value < 0) {
|
|
388
400
|
throw new MetricsError("metrics/counter-decrement",
|
|
389
401
|
"counter.inc value must be >= 0 (got " + arg.value + ") — counters never decrease");
|
|
@@ -454,7 +466,7 @@ function create(opts) {
|
|
|
454
466
|
values: values,
|
|
455
467
|
set: function (callLabels, v) {
|
|
456
468
|
var arg = _normalizeLabelArg(callLabels, v, NaN);
|
|
457
|
-
if (typeof arg.value !== "number" ||
|
|
469
|
+
if (typeof arg.value !== "number" || !isFinite(arg.value)) {
|
|
458
470
|
throw new MetricsError("metrics/gauge-bad-value",
|
|
459
471
|
"gauge.set value must be a finite number");
|
|
460
472
|
}
|
|
@@ -520,7 +532,7 @@ function create(opts) {
|
|
|
520
532
|
values: values,
|
|
521
533
|
observe: function (callLabels, v, exemplar) {
|
|
522
534
|
var arg = _normalizeLabelArg(callLabels, v, NaN);
|
|
523
|
-
if (typeof arg.value !== "number" ||
|
|
535
|
+
if (typeof arg.value !== "number" || !isFinite(arg.value)) {
|
|
524
536
|
throw new MetricsError("metrics/histogram-bad-value",
|
|
525
537
|
"histogram.observe value must be a finite number");
|
|
526
538
|
}
|
|
@@ -157,8 +157,12 @@ function _parseChecksumHeader(headerValue, allowedSet) {
|
|
|
157
157
|
var algo = kvp.key;
|
|
158
158
|
var digestB64 = kvp.value.trim();
|
|
159
159
|
if (algo.length === 0 || digestB64.length === 0) return { error: "malformed" };
|
|
160
|
-
|
|
160
|
+
// hasOwnProperty-guarded lookups — a bare `allowedSet[algo]` resolves
|
|
161
|
+
// Object.prototype for algo="__proto__"/"constructor" (truthy), bypassing the
|
|
162
|
+
// unsupported-algo guard and handing createHash a non-string → HTTP 500.
|
|
163
|
+
if (!Object.prototype.hasOwnProperty.call(allowedSet, algo)) return { error: "algo-unsupported" };
|
|
161
164
|
if (!/^[A-Za-z0-9+/=]+$/.test(digestB64)) return { error: "malformed" };
|
|
165
|
+
if (!Object.prototype.hasOwnProperty.call(KNOWN_CHECKSUM_ALGORITHMS, algo)) return { error: "algo-unsupported" };
|
|
162
166
|
var nodeAlgo = KNOWN_CHECKSUM_ALGORITHMS[algo];
|
|
163
167
|
if (!nodeAlgo) return { error: "algo-unsupported" };
|
|
164
168
|
return { algo: algo, nodeAlgo: nodeAlgo, digestB64: digestB64 };
|
|
@@ -570,7 +574,10 @@ function create(opts) {
|
|
|
570
574
|
if (rec.length === null && req.headers["upload-length"] !== undefined) {
|
|
571
575
|
// Upload-Defer-Length finalization (§4.3) — declare length on first PATCH
|
|
572
576
|
var declared = parseInt(req.headers["upload-length"], 10);
|
|
573
|
-
|
|
577
|
+
// Same strict parse the POST creation path uses — reject trailing junk
|
|
578
|
+
// ("10abc" → 10, "0x10" → 0) rather than parseInt-ing leniently.
|
|
579
|
+
if (!isFinite(declared) || declared < 0 ||
|
|
580
|
+
String(declared) !== String(req.headers["upload-length"]).trim()) {
|
|
574
581
|
return _writeError(res, STATUS_BAD_REQUEST, "Upload-Length must be a non-negative integer");
|
|
575
582
|
}
|
|
576
583
|
if (maxSize !== undefined && declared > maxSize) {
|
package/lib/network-dns.js
CHANGED
|
@@ -158,8 +158,12 @@ function _cachePutPositive(host, family, value) {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
function _cachePutNegative(host, family, error) {
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
// Effective negative TTL: an explicit cacheNegativeTtlMs applies even when the
|
|
162
|
+
// positive TTL is 0 (setCacheTtlMs(0, 60000) = positives off, negatives 60s);
|
|
163
|
+
// only fall back to the positive TTL when no explicit negative TTL is set.
|
|
164
|
+
var ttl = STATE.cacheNegativeTtlMs > 0 ? STATE.cacheNegativeTtlMs
|
|
165
|
+
: (STATE.cacheTtlMs > 0 ? Math.min(STATE.cacheTtlMs, C.TIME.seconds(30)) : 0);
|
|
166
|
+
if (ttl <= 0) return;
|
|
163
167
|
NEGATIVE_CACHE.set(host + "/" + family, {
|
|
164
168
|
error: error,
|
|
165
169
|
expiresAt: _now() + ttl,
|
|
@@ -237,6 +241,14 @@ function setCacheTtlMs(ms, negativeMs) {
|
|
|
237
241
|
"dns.setCacheTtlMs negativeMs: expected non-negative finite number, got " + JSON.stringify(negativeMs));
|
|
238
242
|
}
|
|
239
243
|
STATE.cacheNegativeTtlMs = negativeMs;
|
|
244
|
+
} else {
|
|
245
|
+
// No explicit negative TTL this call — clear any stale explicit value so it
|
|
246
|
+
// can't outlive the setCacheTtlMs call that set it. Otherwise
|
|
247
|
+
// setCacheTtlMs(0) after an earlier setCacheTtlMs(ms, negMs) would leave
|
|
248
|
+
// negative caching alive (sticky transient failures) even though caching was
|
|
249
|
+
// just disabled; with this cleared, negative TTL derives from the positive
|
|
250
|
+
// TTL, so ms=0 disables both positive and negative caching.
|
|
251
|
+
STATE.cacheNegativeTtlMs = 0;
|
|
240
252
|
}
|
|
241
253
|
if (ms === 0) _clearCache();
|
|
242
254
|
}
|
|
@@ -1696,6 +1708,13 @@ async function resolveAaaa(host, opts) { return _resolveProtocol(host, 6, opts);
|
|
|
1696
1708
|
// `type` defaults to "A"; "AAAA" routes through resolve6; SVCB / HTTPS
|
|
1697
1709
|
// types route through the new querySvcb / queryHttps primitives.
|
|
1698
1710
|
async function resolve(host, type, opts) {
|
|
1711
|
+
// A non-string `type` would throw a raw TypeError from .toUpperCase() (code
|
|
1712
|
+
// undefined, no .permanent) — reject it as a typed DnsError like every other
|
|
1713
|
+
// bad-type path, so a caller keying on err.permanent gets the right verdict.
|
|
1714
|
+
if (type !== undefined && type !== null && typeof type !== "string") {
|
|
1715
|
+
throw new DnsError("dns/unsupported-type",
|
|
1716
|
+
"dns.resolve: type must be a string (got " + typeof type + ")");
|
|
1717
|
+
}
|
|
1699
1718
|
type = (type || "A").toUpperCase();
|
|
1700
1719
|
if (type === "A") return _resolveProtocol(host, 4, opts);
|
|
1701
1720
|
if (type === "AAAA") return _resolveProtocol(host, 6, opts);
|
package/lib/parsers/safe-yaml.js
CHANGED
|
@@ -262,6 +262,14 @@ function parse(input, opts) {
|
|
|
262
262
|
return _parseBlockSequence(k, indent, depth);
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
// Root-level flow collection ({...} / [...]) — recognize the flow bracket
|
|
266
|
+
// BEFORE the block-mapping key scan. Otherwise a root JSON/flow object like
|
|
267
|
+
// '{a: 1, b: 2}' matches the block-mapping key scan (the first ': ' makes
|
|
268
|
+
// '{a' look like a bare key) and silently returns structurally-wrong data.
|
|
269
|
+
if (content.charAt(0) === "{" || content.charAt(0) === "[") {
|
|
270
|
+
return { value: _parseInlineValue(content, firstLine.lineNumber, indent), nextLine: k + 1 };
|
|
271
|
+
}
|
|
272
|
+
|
|
265
273
|
// Block mapping starts with a key (bare or quoted) followed by `:`.
|
|
266
274
|
var keyRange = _scanKeyRange(content, firstLine.lineNumber, indent);
|
|
267
275
|
if (keyRange) {
|
|
@@ -493,8 +501,19 @@ function parse(input, opts) {
|
|
|
493
501
|
// Strip trailing whitespace/comment already done by caller (mostly).
|
|
494
502
|
// Handle: flow [...] / {...}, quoted strings, plain scalars.
|
|
495
503
|
var t = text;
|
|
496
|
-
if (t.charAt(0) === "["
|
|
497
|
-
|
|
504
|
+
if (t.charAt(0) === "[" || t.charAt(0) === "{") {
|
|
505
|
+
// Reject trailing junk after a flow collection (matching the quoted-scalar
|
|
506
|
+
// branch below) using _parseFlowValue's end position, then return the value
|
|
507
|
+
// via the direct parser so the shape is identical to the pre-existing path.
|
|
508
|
+
var fend = _parseFlowValue(t, 0, lineNumber, col, 0).nextPos;
|
|
509
|
+
var afterFlow = t.slice(fend).replace(/^\s+/, "");
|
|
510
|
+
if (afterFlow.length > 0 && afterFlow.charAt(0) !== "#") {
|
|
511
|
+
throw new SafeYamlError("unexpected content after flow collection",
|
|
512
|
+
"yaml/trailing-content", lineNumber, col);
|
|
513
|
+
}
|
|
514
|
+
return t.charAt(0) === "[" ? _parseFlowSequence(t, lineNumber, col, 0)
|
|
515
|
+
: _parseFlowMapping(t, lineNumber, col, 0);
|
|
516
|
+
}
|
|
498
517
|
if (t.charAt(0) === '"') {
|
|
499
518
|
var dq = _decodeDoubleQuoted(t, lineNumber, col);
|
|
500
519
|
var afterDq = _trailingAfterQuoted(t, '"');
|
|
@@ -570,6 +589,18 @@ function parse(input, opts) {
|
|
|
570
589
|
throw new SafeYamlError("forbidden key '" + key + "'",
|
|
571
590
|
"yaml/poisoned-key", lineNumber, col + p);
|
|
572
591
|
}
|
|
592
|
+
// Mirror the block-mapping guards so flow style can't bypass them:
|
|
593
|
+
// the merge key '<<' is banned, and a duplicate key is refused (block
|
|
594
|
+
// rejects both; flow previously accepted '<<' as a literal key and
|
|
595
|
+
// silently kept the LAST value on a repeat — a config-override risk).
|
|
596
|
+
if (key === "<<") {
|
|
597
|
+
throw new SafeYamlError("merge key '<<' not supported (anchor-using feature)",
|
|
598
|
+
"yaml/merge-key-banned", lineNumber, col + p);
|
|
599
|
+
}
|
|
600
|
+
if (Object.prototype.hasOwnProperty.call(result, key)) {
|
|
601
|
+
throw new SafeYamlError("duplicate mapping key '" + key + "'",
|
|
602
|
+
"yaml/duplicate-key", lineNumber, col + p);
|
|
603
|
+
}
|
|
573
604
|
_bumpKeys(lineNumber);
|
|
574
605
|
p = keyVal.nextPos;
|
|
575
606
|
p = _flowSkipWsIndex(text, p);
|
package/lib/safe-buffer.js
CHANGED
|
@@ -69,7 +69,17 @@ class SafeBufferError extends FrameworkError {
|
|
|
69
69
|
|
|
70
70
|
function _throw(errorClass, message, code) {
|
|
71
71
|
var Cls = errorClass || SafeBufferError;
|
|
72
|
-
|
|
72
|
+
var err = new Cls(message, code);
|
|
73
|
+
// SafeBufferError takes (message, code) but a defineClass errorClass (the
|
|
74
|
+
// convention for every framework-error, e.g. a caller's TusError) takes
|
|
75
|
+
// (code, message) — the opposite order — so `new Cls(message, code)` would
|
|
76
|
+
// SWAP .code/.message for such a class (a caller branching on e.code then
|
|
77
|
+
// never matches). Set both fields explicitly so they are correct whichever
|
|
78
|
+
// constructor order the passed class uses; the class-derived flags are set
|
|
79
|
+
// from the class options, not the code value, so this is safe.
|
|
80
|
+
err.code = code;
|
|
81
|
+
err.message = message;
|
|
82
|
+
throw err;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
/**
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:286e751e-3a05-41d2-9523-c516934c46dc",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-04T05:11:23.972Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.16.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.16.4",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.16.
|
|
25
|
+
"version": "0.16.4",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.16.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.16.4",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.16.
|
|
57
|
+
"ref": "@blamejs/core@0.16.4",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|