@blamejs/blamejs-shop 0.4.68 → 0.4.70
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/admin.js +8 -6
- package/lib/asset-manifest.json +1 -1
- package/lib/giftcards.js +29 -5
- package/lib/security-middleware.js +65 -23
- package/lib/storefront.js +10 -7
- package/lib/vendor/MANIFEST.json +319 -267
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +58 -5
- package/lib/vendor/blamejs/examples/wiki/README.md +1 -1
- package/lib/vendor/blamejs/examples/wiki/docker-compose.prod.yml +9 -8
- package/lib/vendor/blamejs/examples/wiki/docker-compose.yml +10 -9
- package/lib/vendor/blamejs/examples/wiki/env-snapshot.json +4 -3
- package/lib/vendor/blamejs/examples/wiki/lib/build-app.js +33 -17
- package/lib/vendor/blamejs/examples/wiki/routes/admin.js +7 -10
- package/lib/vendor/blamejs/examples/wiki/server.js +5 -4
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +5 -0
- package/lib/vendor/blamejs/lib/a2a-tasks.js +38 -6
- package/lib/vendor/blamejs/lib/agent-event-bus.js +13 -0
- package/lib/vendor/blamejs/lib/agent-idempotency.js +5 -1
- package/lib/vendor/blamejs/lib/agent-snapshot.js +32 -2
- package/lib/vendor/blamejs/lib/ai-aedt-bias-audit.js +2 -1
- package/lib/vendor/blamejs/lib/ai-content-detect.js +1 -3
- package/lib/vendor/blamejs/lib/ai-frontier-protocol.js +1 -1
- package/lib/vendor/blamejs/lib/ai-model-manifest.js +1 -1
- package/lib/vendor/blamejs/lib/ai-output.js +16 -7
- package/lib/vendor/blamejs/lib/api-snapshot.js +4 -1
- package/lib/vendor/blamejs/lib/app-shutdown.js +7 -1
- package/lib/vendor/blamejs/lib/archive-gz.js +9 -0
- package/lib/vendor/blamejs/lib/archive-read.js +9 -7
- package/lib/vendor/blamejs/lib/archive-tar-read.js +51 -8
- package/lib/vendor/blamejs/lib/archive.js +4 -2
- package/lib/vendor/blamejs/lib/asn1-der.js +70 -22
- package/lib/vendor/blamejs/lib/atomic-file.js +204 -2
- package/lib/vendor/blamejs/lib/audit-chain.js +54 -5
- package/lib/vendor/blamejs/lib/audit-daily-review.js +12 -2
- package/lib/vendor/blamejs/lib/audit-sign.js +2 -1
- package/lib/vendor/blamejs/lib/audit-tools.js +108 -23
- package/lib/vendor/blamejs/lib/audit.js +7 -2
- package/lib/vendor/blamejs/lib/auth/access-lock.js +2 -2
- package/lib/vendor/blamejs/lib/auth/bot-challenge.js +1 -1
- package/lib/vendor/blamejs/lib/auth/ciba.js +43 -4
- package/lib/vendor/blamejs/lib/auth/dpop.js +6 -1
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +5 -1
- package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
- package/lib/vendor/blamejs/lib/auth/lockout.js +18 -2
- package/lib/vendor/blamejs/lib/auth/passkey.js +1 -1
- package/lib/vendor/blamejs/lib/auth/password.js +1 -1
- package/lib/vendor/blamejs/lib/auth/saml.js +33 -13
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +24 -4
- package/lib/vendor/blamejs/lib/auth/status-list.js +14 -2
- package/lib/vendor/blamejs/lib/auth/step-up.js +9 -1
- package/lib/vendor/blamejs/lib/auth-bot-challenge.js +21 -2
- package/lib/vendor/blamejs/lib/backup/bundle.js +7 -2
- package/lib/vendor/blamejs/lib/backup/crypto.js +23 -8
- package/lib/vendor/blamejs/lib/backup/index.js +41 -20
- package/lib/vendor/blamejs/lib/backup/manifest.js +7 -1
- package/lib/vendor/blamejs/lib/break-glass.js +41 -22
- package/lib/vendor/blamejs/lib/cbor.js +34 -11
- package/lib/vendor/blamejs/lib/cdn-cache-control.js +7 -3
- package/lib/vendor/blamejs/lib/cert.js +5 -3
- package/lib/vendor/blamejs/lib/cli.js +5 -1
- package/lib/vendor/blamejs/lib/cloud-events.js +3 -2
- package/lib/vendor/blamejs/lib/cluster-storage.js +7 -3
- package/lib/vendor/blamejs/lib/codepoint-class.js +17 -0
- package/lib/vendor/blamejs/lib/compliance-eaa.js +1 -1
- package/lib/vendor/blamejs/lib/compliance-sanctions.js +9 -7
- package/lib/vendor/blamejs/lib/compliance.js +1 -1
- package/lib/vendor/blamejs/lib/config-drift.js +22 -8
- package/lib/vendor/blamejs/lib/content-credentials.js +11 -8
- package/lib/vendor/blamejs/lib/content-digest.js +11 -4
- package/lib/vendor/blamejs/lib/cookies.js +10 -2
- package/lib/vendor/blamejs/lib/cose.js +20 -0
- package/lib/vendor/blamejs/lib/crdt.js +2 -1
- package/lib/vendor/blamejs/lib/crypto-field.js +29 -23
- package/lib/vendor/blamejs/lib/crypto.js +18 -4
- package/lib/vendor/blamejs/lib/csp.js +4 -0
- package/lib/vendor/blamejs/lib/daemon.js +4 -1
- package/lib/vendor/blamejs/lib/data-act.js +27 -4
- package/lib/vendor/blamejs/lib/db-file-lifecycle.js +14 -6
- package/lib/vendor/blamejs/lib/db-query.js +69 -9
- package/lib/vendor/blamejs/lib/db.js +32 -15
- package/lib/vendor/blamejs/lib/dora.js +43 -13
- package/lib/vendor/blamejs/lib/dr-runbook.js +1 -1
- package/lib/vendor/blamejs/lib/dsa.js +2 -1
- package/lib/vendor/blamejs/lib/dsr.js +22 -8
- package/lib/vendor/blamejs/lib/early-hints.js +19 -0
- package/lib/vendor/blamejs/lib/eat.js +5 -1
- package/lib/vendor/blamejs/lib/external-db.js +60 -4
- package/lib/vendor/blamejs/lib/fda-21cfr11.js +30 -5
- package/lib/vendor/blamejs/lib/flag-providers.js +6 -2
- package/lib/vendor/blamejs/lib/forms.js +1 -1
- package/lib/vendor/blamejs/lib/gate-contract.js +46 -5
- package/lib/vendor/blamejs/lib/gdpr-ropa.js +18 -9
- package/lib/vendor/blamejs/lib/graphql-federation.js +17 -4
- package/lib/vendor/blamejs/lib/guard-all.js +2 -2
- package/lib/vendor/blamejs/lib/guard-dsn.js +1 -1
- package/lib/vendor/blamejs/lib/guard-envelope.js +1 -1
- package/lib/vendor/blamejs/lib/guard-html.js +9 -11
- package/lib/vendor/blamejs/lib/guard-imap-command.js +1 -1
- package/lib/vendor/blamejs/lib/guard-jmap.js +1 -1
- package/lib/vendor/blamejs/lib/guard-json.js +14 -6
- package/lib/vendor/blamejs/lib/guard-mail-move.js +1 -1
- package/lib/vendor/blamejs/lib/guard-managesieve-command.js +1 -1
- package/lib/vendor/blamejs/lib/guard-pop3-command.js +1 -1
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +1 -1
- package/lib/vendor/blamejs/lib/guard-svg.js +8 -9
- package/lib/vendor/blamejs/lib/html-balance.js +7 -3
- package/lib/vendor/blamejs/lib/http-client-cookie-jar.js +33 -12
- package/lib/vendor/blamejs/lib/http-client.js +225 -53
- package/lib/vendor/blamejs/lib/iab-tcf.js +3 -2
- package/lib/vendor/blamejs/lib/importmap-integrity.js +41 -1
- package/lib/vendor/blamejs/lib/incident-report.js +9 -6
- package/lib/vendor/blamejs/lib/json-patch.js +1 -1
- package/lib/vendor/blamejs/lib/json-path.js +24 -3
- package/lib/vendor/blamejs/lib/jtd.js +2 -2
- package/lib/vendor/blamejs/lib/legal-hold.js +24 -8
- package/lib/vendor/blamejs/lib/log.js +2 -2
- package/lib/vendor/blamejs/lib/mail-agent.js +2 -2
- package/lib/vendor/blamejs/lib/mail-arf.js +1 -1
- package/lib/vendor/blamejs/lib/mail-auth.js +3 -3
- package/lib/vendor/blamejs/lib/mail-bimi.js +16 -16
- package/lib/vendor/blamejs/lib/mail-bounce.js +3 -3
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +71 -6
- package/lib/vendor/blamejs/lib/mail-deploy.js +9 -5
- package/lib/vendor/blamejs/lib/mail-greylist.js +2 -4
- package/lib/vendor/blamejs/lib/mail-helo.js +2 -4
- package/lib/vendor/blamejs/lib/mail-journal.js +11 -8
- package/lib/vendor/blamejs/lib/mail-mdn.js +8 -4
- package/lib/vendor/blamejs/lib/mail-rbl.js +2 -4
- package/lib/vendor/blamejs/lib/mail-scan.js +3 -5
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +4 -1
- package/lib/vendor/blamejs/lib/mail-server-registry.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-tls.js +9 -2
- package/lib/vendor/blamejs/lib/mail-spam-score.js +2 -4
- package/lib/vendor/blamejs/lib/mail-store-fts.js +1 -1
- package/lib/vendor/blamejs/lib/mail.js +22 -2
- package/lib/vendor/blamejs/lib/markup-tokenizer.js +24 -0
- package/lib/vendor/blamejs/lib/mcp.js +6 -4
- package/lib/vendor/blamejs/lib/mdoc.js +26 -3
- package/lib/vendor/blamejs/lib/metrics.js +14 -3
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +2 -2
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +10 -4
- package/lib/vendor/blamejs/lib/middleware/bot-guard.js +26 -18
- package/lib/vendor/blamejs/lib/middleware/clear-site-data.js +5 -1
- package/lib/vendor/blamejs/lib/middleware/compression.js +9 -0
- package/lib/vendor/blamejs/lib/middleware/cors.js +32 -23
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +60 -21
- package/lib/vendor/blamejs/lib/middleware/daily-byte-quota.js +6 -4
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +28 -4
- package/lib/vendor/blamejs/lib/middleware/network-allowlist.js +61 -30
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +25 -16
- package/lib/vendor/blamejs/lib/middleware/scim-server.js +2 -1
- package/lib/vendor/blamejs/lib/middleware/security-headers.js +24 -6
- package/lib/vendor/blamejs/lib/middleware/speculation-rules.js +6 -3
- package/lib/vendor/blamejs/lib/middleware/tus-upload.js +2 -2
- package/lib/vendor/blamejs/lib/money.js +1 -1
- package/lib/vendor/blamejs/lib/mtls-ca.js +10 -6
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +1 -1
- package/lib/vendor/blamejs/lib/network-dns.js +9 -2
- package/lib/vendor/blamejs/lib/network-dnssec.js +2 -1
- package/lib/vendor/blamejs/lib/network-smtp-policy.js +23 -5
- package/lib/vendor/blamejs/lib/network-tls.js +27 -5
- package/lib/vendor/blamejs/lib/network-tsig.js +2 -2
- package/lib/vendor/blamejs/lib/nis2-report.js +1 -1
- package/lib/vendor/blamejs/lib/nist-crosswalk.js +1 -1
- package/lib/vendor/blamejs/lib/ntp-check.js +28 -0
- package/lib/vendor/blamejs/lib/numeric-bounds.js +9 -0
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +1 -2
- package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +4 -2
- package/lib/vendor/blamejs/lib/object-store/gcs.js +6 -4
- package/lib/vendor/blamejs/lib/object-store/http-put.js +1 -2
- package/lib/vendor/blamejs/lib/object-store/http-request.js +30 -1
- package/lib/vendor/blamejs/lib/object-store/local.js +37 -17
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +1 -2
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +20 -4
- package/lib/vendor/blamejs/lib/outbox.js +11 -4
- package/lib/vendor/blamejs/lib/parsers/safe-xml.js +1 -1
- package/lib/vendor/blamejs/lib/parsers/safe-yaml.js +21 -3
- package/lib/vendor/blamejs/lib/queue-local.js +10 -3
- package/lib/vendor/blamejs/lib/redact.js +7 -3
- package/lib/vendor/blamejs/lib/request-helpers.js +201 -23
- package/lib/vendor/blamejs/lib/resource-access-lock.js +3 -3
- package/lib/vendor/blamejs/lib/restore-bundle.js +46 -18
- package/lib/vendor/blamejs/lib/restore-rollback.js +10 -4
- package/lib/vendor/blamejs/lib/restore.js +19 -0
- package/lib/vendor/blamejs/lib/retention.js +20 -4
- package/lib/vendor/blamejs/lib/router.js +17 -4
- package/lib/vendor/blamejs/lib/safe-ical.js +2 -2
- package/lib/vendor/blamejs/lib/safe-icap.js +1 -1
- package/lib/vendor/blamejs/lib/safe-json.js +44 -0
- package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
- package/lib/vendor/blamejs/lib/safe-vcard.js +1 -1
- package/lib/vendor/blamejs/lib/sandbox-worker.js +6 -0
- package/lib/vendor/blamejs/lib/sandbox.js +1 -1
- package/lib/vendor/blamejs/lib/scheduler.js +17 -1
- package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +16 -0
- package/lib/vendor/blamejs/lib/session.js +27 -3
- package/lib/vendor/blamejs/lib/sql.js +3 -3
- package/lib/vendor/blamejs/lib/static.js +65 -13
- package/lib/vendor/blamejs/lib/template.js +7 -5
- package/lib/vendor/blamejs/lib/tenant-quota.js +52 -19
- package/lib/vendor/blamejs/lib/tsa.js +5 -2
- package/lib/vendor/blamejs/lib/vault/index.js +5 -0
- package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +22 -26
- package/lib/vendor/blamejs/lib/vault/passphrase-source.js +8 -3
- package/lib/vendor/blamejs/lib/vault/rotate.js +13 -18
- package/lib/vendor/blamejs/lib/vault/seal-pem-file.js +4 -1
- package/lib/vendor/blamejs/lib/vc.js +1 -1
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +23 -10
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +5498 -5494
- package/lib/vendor/blamejs/lib/webhook.js +16 -1
- package/lib/vendor/blamejs/lib/websocket.js +1 -1
- package/lib/vendor/blamejs/lib/worm.js +1 -1
- package/lib/vendor/blamejs/lib/ws-client.js +57 -46
- package/lib/vendor/blamejs/lib/x509-chain.js +44 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.14.json +122 -0
- package/lib/vendor/blamejs/scripts/check-vendor-currency.js +119 -4
- package/lib/vendor/blamejs/test/00-primitives.js +5 -1
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +18 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/a2a-tasks.test.js +42 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +36 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-snapshot.test.js +47 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-gz.test.js +24 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-tar-hardening.test.js +160 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/asn1-der.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-fd-read.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-nofollow.test.js +79 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-write-excl.test.js +132 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-write-stream.test.js +181 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-chain-corrupted-anchor.test.js +65 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-chain-incremental-verify.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-daily-review.test.js +48 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js +13 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-verifybundle-tamper.test.js +122 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-bot-challenge.test.js +37 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +5 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-lockout.test.js +21 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-status-list.test.js +73 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/bot-guard.test.js +17 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +62 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/clear-site-data.test.js +12 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cluster-storage.test.js +40 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +146 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-sanctions.test.js +14 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compression-range.test.js +115 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +17 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/cors.test.js +27 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-per-row-key.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/data-act.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dora.test.js +20 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-alg-kty.test.js +64 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fda-21cfr11.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +33 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-html.test.js +34 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-json.test.js +12 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +72 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-throttle-transform.test.js +88 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/importmap-integrity.test.js +25 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/legal-hold.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +20 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-header-injection.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +63 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/mdoc.test.js +41 -14
- package/lib/vendor/blamejs/test/layer-0-primitives/network-allowlist.test.js +61 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/object-store-range-header.test.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +35 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/output-header-hardening.test.js +102 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parser-verify-hardening.test.js +191 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/proto-shadow-allowlist.test.js +120 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-xff-spoofing.test.js +75 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/restore-blob-remap.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-sweep-termination.test.js +104 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-body-validation.test.js +98 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-json-stringify-for-script.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +48 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +23 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +54 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +24 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +42 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/step-up.test.js +7 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/template-escape-html.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tenant-quota.test.js +15 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault-default-store.test.js +48 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vendor-currency-classify.test.js +77 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/x509-chain-ca-enforcement.test.js +149 -0
- package/lib/vendor/blamejs/test/layer-5-integration/external-db-residency.test.js +34 -0
- package/package.json +1 -1
|
@@ -1647,12 +1647,16 @@ function testFormatValidatorLengthCap() {
|
|
|
1647
1647
|
var line = lines[li];
|
|
1648
1648
|
if (/^\s*(\/\/|\*|\/\*)/.test(line)) continue;
|
|
1649
1649
|
if (!/\b[A-Z_]+_RE\.test\(/.test(line)) continue;
|
|
1650
|
-
// Look for
|
|
1651
|
-
//
|
|
1652
|
-
//
|
|
1650
|
+
// Look for a length bound in the surrounding 5 lines (any comparison:
|
|
1651
|
+
// <, >, =, !=, !==, including the explicit-length shape `s.length !==
|
|
1652
|
+
// expectedLength`). A byte-length bound (Buffer.byteLength /
|
|
1653
|
+
// safeBuffer.byteLengthOf) counts too — the byte-cap-vs-char-length
|
|
1654
|
+
// hardening replaces `.length` caps with byte-length on multibyte input,
|
|
1655
|
+
// and `byteLength(x) > MAX || RE.test(x)` short-circuits the regex on
|
|
1656
|
+
// anything over the cap just as `.length` does.
|
|
1653
1657
|
var window = (lines[li-2] || "") + (lines[li-1] || "") +
|
|
1654
1658
|
line + (lines[li+1] || "") + (lines[li+2] || "");
|
|
1655
|
-
if (/\.length\s*[><=!]/.test(window)) continue;
|
|
1659
|
+
if (/\.length\s*[><=!]/.test(window) || /byteLength/.test(window)) continue;
|
|
1656
1660
|
bad.push({
|
|
1657
1661
|
file: _relPath(files[fi]),
|
|
1658
1662
|
line: li + 1,
|
|
@@ -3850,13 +3854,24 @@ async function testNoDuplicateCodeBlocks() {
|
|
|
3850
3854
|
{ mode: "family-subset", files: ["lib/ai-dp.js:mechanism", "lib/dora.js:_validateReportInput", "lib/guard-snapshot-envelope.js:validate"] }, // fp:b0dceb3bd2fe
|
|
3851
3855
|
{ mode: "family-subset", files: ["lib/auth/sd-jwt-vc-issuer.js:create", "lib/fsm.js:define", "lib/mail.js:_validateMessage"] }, // fp:dff4eab2b4e0
|
|
3852
3856
|
{ mode: "family-subset", files: ["lib/csp.js:_parsePermissionsPolicyString", "lib/http-message-signature.js:_parseSignatureInput", "lib/network-tls.js:_parseSanString"] }, // fp:c998d327f9c0
|
|
3853
|
-
{ mode: "family-subset", files: ["lib/middleware/idempotency-key.js:resealMigrate", "lib/object-store/http-request.js:
|
|
3857
|
+
{ mode: "family-subset", files: ["lib/middleware/idempotency-key.js:resealMigrate", "lib/object-store/http-request.js:promiseToStream", "lib/validate-opts.js:observabilityShape"] }, // fp:824f10cf9f10 (was notModifiedGetResult/fp:711a58281dd7 before promiseToStream landed adjacent — coincidental object-literal/return shingle across 3 unrelated fns)
|
|
3854
3858
|
{ mode: "family-subset", files: ["lib/auth/sd-jwt-vc-issuer.js:create", "lib/guard-saga-config.js:validate", "lib/network-heartbeat.js:_validateTarget"] }, // fp:c8f43d4d1941
|
|
3855
3859
|
{ mode: "family-subset", files: ["lib/auth/oauth.js:deviceAuthorization", "lib/auth/oauth.js:parseCallback", "lib/ddl-change-control.js:_hashSql", "lib/mail-rbl.js:query"] }, // fp:882fd32d8e11
|
|
3856
3860
|
{ mode: "family-subset", files: ["lib/auth/oid4vp.js:matchDcql", "lib/gate-contract.js:_ctxValueForKind", "lib/http-message-signature.js:_parseUrl"] }, // fp:726ed545b065
|
|
3857
3861
|
{ mode: "family-subset", files: ["lib/archive-adapters.js:close", "lib/crypto-field.js:listPerRowResidency", "lib/tracing.js:spanSync"] }, // fp:c4617588ed77
|
|
3858
3862
|
{ mode: "family-subset", files: ["lib/backup/bundle.js:create", "lib/mail-greylist.js:check", "lib/safe-buffer.js:makeByteCoercer"] }, // fp:d4c06878dcda
|
|
3859
3863
|
{ mode: "family-subset", files: ["lib/breach-deadline.js:trackReport", "lib/guard-snapshot-envelope.js:validate", "lib/incident-report.js:open"] }, // fp:d7d8a89732c8
|
|
3864
|
+
// Proto-shadow hardening converged these allowlist gates onto the framework's
|
|
3865
|
+
// canonical `!Object.prototype.hasOwnProperty.call(MAP, key)` membership idiom
|
|
3866
|
+
// (the 312-use convention), which pushed the shared validation preamble past
|
|
3867
|
+
// the 50-token shingle. Shape-only: the maps, error classes, codes, and
|
|
3868
|
+
// messages diverge per domain (mail-spam-score lists the valid profiles in
|
|
3869
|
+
// its bad-profile message; guard-envelope does not — a deliberate per-caller
|
|
3870
|
+
// choice, see gate-contract.resolveProfileName which owns only the resolution
|
|
3871
|
+
// expression and leaves each caller its bespoke typed throw).
|
|
3872
|
+
{ mode: "family-subset", files: ["lib/guard-envelope.js:check", "lib/mail-greylist.js:create", "lib/mail-helo.js:evaluate", "lib/mail-rbl.js:create", "lib/mail-scan.js:create", "lib/mail-spam-score.js:create"] }, // fp:958143c0319d (was fp:59fd2e7ac678 before mail-scan's profile gate joined)
|
|
3873
|
+
{ mode: "family-subset", files: ["lib/mail-journal.js:_validateRegimes", "lib/middleware/tus-upload.js:create", "lib/safe-sieve.js:_parseCommand"] }, // fp:2524f7ddf5a1
|
|
3874
|
+
{ mode: "family-subset", files: ["lib/breach-deadline.js:open", "lib/breach-deadline.js:trackReport", "lib/cms-codec.js:encodeEnvelopedData", "lib/cms-codec.js:encodeSignedData", "lib/mail-deploy.js:mtaStsPublish"] }, // fp:32fba36fb64e
|
|
3860
3875
|
{
|
|
3861
3876
|
// presigned-policy expiry-range validation + structure — shape-only. `var
|
|
3862
3877
|
// ttl = opts.expiresIn != null ? opts.expiresIn : <default>; if (typeof ttl
|
|
@@ -5092,6 +5107,89 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
5092
5107
|
allowlist: ["lib/guard-auth.js"],
|
|
5093
5108
|
reason: "v0.15.0 #103 — the guard sanitize/parse refuse-on-critical|high throw (err(issue.ruleId || '<x>.refused', 'guard<Name>.<op>: ' + issue.snippet)) is owned by gateContract.throwOnRefusalSeverity; 18 guards reuse it (this was the failing STRONG-DUP fp:f349a8d1f51b before extraction). A hand-rolled `issues[i].ruleId || '<x>.refused'` throw re-implements it. lib/guard-auth.js is the one genuine holdout (its message embeds issues[i].source: 'guardAuth.sanitize [<source>]:') pending task #104; the primitive itself uses a `fallback` variable (no .refused literal) so it does not match. Any other lib file with this shape must call gateContract.throwOnRefusalSeverity (the severities / op options cover the critical-only + parse variants).",
|
|
5094
5109
|
},
|
|
5110
|
+
{
|
|
5111
|
+
id: "html-comment-scan-must-use-htmlCommentEnd",
|
|
5112
|
+
primitive: "b.markupTokenizer.htmlCommentEnd",
|
|
5113
|
+
scanScope: "lib",
|
|
5114
|
+
skipCommentLines: true,
|
|
5115
|
+
// The WHATWG HTML tokenizer closes a comment at "--!>" and ABRUPTLY at
|
|
5116
|
+
// "<!-->" / "<!--->", not only "-->". An HTML/SVG-in-HTML scanner that
|
|
5117
|
+
// searches indexOf("-->") disagrees with the browser on the comment
|
|
5118
|
+
// boundary, so an element after an early terminator is swallowed as inert
|
|
5119
|
+
// comment by the sanitizer but parsed LIVE by the browser (mXSS, the
|
|
5120
|
+
// comment-parser differential). markupTokenizer.htmlCommentEnd owns the
|
|
5121
|
+
// correct scan; guard-html / guard-svg / mail-bimi / html-balance route
|
|
5122
|
+
// through it. XML comment scanners (safe-xml, xml-c14n) legitimately use
|
|
5123
|
+
// "-->" because XML has neither "--!>" nor abrupt-close forms.
|
|
5124
|
+
regex: /\.indexOf\("-->"/,
|
|
5125
|
+
allowlist: ["lib/markup-tokenizer.js", "lib/parsers/safe-xml.js", "lib/xml-c14n.js"],
|
|
5126
|
+
reason: "HTML/SVG-in-HTML comment scanning must use markupTokenizer.htmlCommentEnd (covers --!> + abrupt <!-->/<!--->), not a bare indexOf('-->'). XML scanners are exempt (XML has no such forms); markup-tokenizer.js is the primitive's home.",
|
|
5127
|
+
},
|
|
5128
|
+
{
|
|
5129
|
+
id: "x509-issuer-check-must-enforce-ca",
|
|
5130
|
+
primitive: "b.x509Chain.issuerValidlyIssued",
|
|
5131
|
+
scanScope: "lib",
|
|
5132
|
+
skipCommentLines: true,
|
|
5133
|
+
// node:crypto X509Certificate.checkIssued() does NOT enforce
|
|
5134
|
+
// basicConstraints cA:TRUE, so `subject.checkIssued(issuer)` used to
|
|
5135
|
+
// accept a leaf / end-entity cert (cA:FALSE, no keyUsage) as a signing
|
|
5136
|
+
// CA — the basicConstraints bypass (CVE-2002-0862 class) that hit
|
|
5137
|
+
// tsa.js / mail-bimi.js / mail-crypto-smime.js. Every chain walker now
|
|
5138
|
+
// routes its issuer test through x509Chain.issuerValidlyIssued (which
|
|
5139
|
+
// adds the cA check). A hand-rolled `receiver.checkIssued(otherArg)`
|
|
5140
|
+
// re-introduces the gap. The negative backref skips the legitimate
|
|
5141
|
+
// self-signed-root probe `current.checkIssued(current)` (same receiver
|
|
5142
|
+
// + arg), which is not an issuer-acceptance use.
|
|
5143
|
+
regex: /\b(\w+)\.checkIssued\s*\(\s*(?!\1[\s,)])\w/,
|
|
5144
|
+
allowlist: ["lib/x509-chain.js"],
|
|
5145
|
+
reason: "basicConstraints cA:TRUE enforcement is owned by x509Chain.issuerValidlyIssued / x509Chain.isCaCert; tsa/mail-bimi/mail-crypto-smime route through it. Any lib file calling X.checkIssued(Y) (Y!=X) directly bypasses the cA check and must use x509Chain instead. lib/x509-chain.js is the home of the primitive.",
|
|
5146
|
+
},
|
|
5147
|
+
{
|
|
5148
|
+
id: "trusted-proxy-cidr-must-canonicalize-peer",
|
|
5149
|
+
primitive: "b.requestHelpers.trustedClientIp",
|
|
5150
|
+
scanScope: "lib",
|
|
5151
|
+
skipCommentLines: true,
|
|
5152
|
+
// The peer-gating trustedProxies predicate matches the immediate peer
|
|
5153
|
+
// address against operator CIDRs. cidrContains refuses a cross-family
|
|
5154
|
+
// compare, so an IPv4-mapped IPv6 peer (::ffff:a.b.c.d from a dual-stack
|
|
5155
|
+
// listener) silently fails an IPv4 CIDR and the proxy reads as untrusted —
|
|
5156
|
+
// X-Forwarded-* is then ignored and the gate keys on the proxy. The peer
|
|
5157
|
+
// MUST be folded through ssrfGuard.canonicalizeHost (var canon) first; a
|
|
5158
|
+
// raw `cidrContains(trustedProxies[i], addr)` re-introduces the bypass.
|
|
5159
|
+
regex: /cidrContains\(trustedProxies\[\w+\],\s*(?!canon\b)\w/,
|
|
5160
|
+
allowlist: [],
|
|
5161
|
+
reason: "Match the immediate peer against trustedProxies only after folding IPv4-mapped IPv6 via ssrfGuard.canonicalizeHost (the `canon` local) — cidrContains rejects a cross-family compare, so a raw peer addr lets a dual-stack proxy read as untrusted. request-helpers.js's _trustedProxyPredicate owns this.",
|
|
5162
|
+
},
|
|
5163
|
+
{
|
|
5164
|
+
id: "http-upload-content-length-must-guard-transform",
|
|
5165
|
+
primitive: "b.httpClient.request",
|
|
5166
|
+
scanScope: "lib",
|
|
5167
|
+
skipCommentLines: true,
|
|
5168
|
+
// A Content-Length set from a Buffer body's length is wrong once a
|
|
5169
|
+
// size-changing uploadTransform (gzip / encrypt / frame) is interposed —
|
|
5170
|
+
// the server truncates extra bytes or waits for bytes that never arrive.
|
|
5171
|
+
// The assignment must be guarded by `uploadTransforms.length === 0` so a
|
|
5172
|
+
// transformed body is framed (chunked / DATA frames) on its real size.
|
|
5173
|
+
regex: /Buffer\.isBuffer\(opts\.body\)(?:(?!uploadTransforms)[\s\S]){0,400}?content-length"\]\s*=\s*(?:String\()?opts\.body\.length/i,
|
|
5174
|
+
allowlist: [],
|
|
5175
|
+
reason: "Set Content-Length from opts.body.length only when uploadTransforms.length === 0 — a size-changing uploadTransform makes the original length wrong (truncation / hang). Omit it (chunked / H2 DATA frames) when a transform is present. lib/http-client.js owns both legs.",
|
|
5176
|
+
},
|
|
5177
|
+
{
|
|
5178
|
+
id: "http-response-collect-must-use-download-pipeline",
|
|
5179
|
+
primitive: "b.httpClient.request",
|
|
5180
|
+
scanScope: "lib",
|
|
5181
|
+
skipCommentLines: true,
|
|
5182
|
+
// The buffered (non-stream) response collector must read from the
|
|
5183
|
+
// _buildDownloadStream pipeline tail, not the raw socket stream — reading
|
|
5184
|
+
// `res`/`stream` directly bypasses maxBytesPerSec + downloadTransform in
|
|
5185
|
+
// buffer / always-resolve mode (the documented default), so a configured
|
|
5186
|
+
// throttle / transform silently no-ops. Anchored on the full-response
|
|
5187
|
+
// collector (maxResponseBytes) so the bounded error-prefix reader is not
|
|
5188
|
+
// matched.
|
|
5189
|
+
regex: /boundedChunkCollector\(\{ maxBytes: maxResponseBytes \}\)(?:(?!_buildDownloadStream)[\s\S]){0,300}?\b(?:res|stream)\.on\("data"/,
|
|
5190
|
+
allowlist: [],
|
|
5191
|
+
reason: "Feed the full-response collector from _buildDownloadStream(...) (the dlSource / dlH2 tail), never raw res/stream .on('data') — otherwise maxBytesPerSec / downloadTransform apply only in stream mode. lib/http-client.js owns both H1 + H2 paths.",
|
|
5192
|
+
},
|
|
5095
5193
|
{
|
|
5096
5194
|
// A hard quota / rate / budget ceiling must be enforced with an
|
|
5097
5195
|
// atomic conditional reserve — the limit test and the charge are
|
|
@@ -8390,6 +8488,49 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
8390
8488
|
allowlist: ["lib/atomic-file.js"],
|
|
8391
8489
|
reason: "A TOCTOU-safe synchronous file read (open fd → fstat → read-fully loop, binding size/content/integrity to the fd's inode against a swap, CWE-367) is owned by b.atomicFile.fdSafeReadSync — with optional guards (maxBytes cap, refuseSymlink + inodeCheck, expectedHash, encoding, allowShortRead) and a per-caller errorFor(kind, detail) so each domain keeps its typed error. The §1 sweep routed the four hand-rolled read-fully loops: atomic-file._readSyncCore (the home; now the wrapper that supplies AtomicFileError messages), network-tls._readPathFile (utf8 + slice-on-short-read + raw ENOENT), vault/seal-pem-file._resealNow (the strongest posture: refuseSymlink + inodeCheck + maxSourceBytes, SealPemFileError codes wrapped by its outer audit/onError catch), backup/bundle.create (BackupBundleError short-read with per-entry relativePath). Allowlist is lib/atomic-file.js (fdSafeReadSync's own loop is the single home); a re-introduced `readSync(fd, buf, read, size - read, null)` read-loop anywhere else trips this — call b.atomicFile.fdSafeReadSync instead.",
|
|
8392
8490
|
},
|
|
8491
|
+
{
|
|
8492
|
+
id: "raw-fs-write-without-exclusive-nofollow",
|
|
8493
|
+
primitive: "b.atomicFile.writeSync / writeStream / writeExclSync — atomic, O_EXCL|O_NOFOLLOW write-replace",
|
|
8494
|
+
// The write-side sibling of fd-read-loop-hand-rolled. A bare
|
|
8495
|
+
// nodeFs.writeFileSync(path, ...) / nodeFs.createWriteStream(path) follows
|
|
8496
|
+
// a symlink an attacker pre-planted at `path` (CWE-59 arbitrary write) and
|
|
8497
|
+
// leaves a torn file at the canonical name on a crash. Every framework
|
|
8498
|
+
// write-replace goes through an atomic-file primitive that stages into a
|
|
8499
|
+
// CSPRNG temp opened O_EXCL|O_NOFOLLOW and renames: writeSync (buffer),
|
|
8500
|
+
// writeStream (streaming source), writeExclSync (staged write→verify→rename
|
|
8501
|
+
// for the vault seal/unseal/rotate round-trip).
|
|
8502
|
+
scanScope: "lib",
|
|
8503
|
+
skipCommentLines: true,
|
|
8504
|
+
regex: /\bnodeFs\.(?:writeFileSync|createWriteStream)\s*\(/,
|
|
8505
|
+
allowlist: [
|
|
8506
|
+
"lib/atomic-file.js", // primitives' home: createWriteStream(null, { fd }) over an _openExclTemp fd (the fd already carries O_EXCL|O_NOFOLLOW)
|
|
8507
|
+
"lib/backup/index.js", // fsAdapter.writeFile uses writeFileSync(..., { flag: "wx" }) — O_CREAT|O_EXCL refuses (EEXIST) a pre-planted file/symlink
|
|
8508
|
+
"lib/http-client.js", // downloadStream: createWriteStream(tmpPath, { flags: O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW }) — explicit exclusive no-follow create
|
|
8509
|
+
],
|
|
8510
|
+
reason: "Symlink-follow / torn-write class (CWE-59 / CWE-377). A raw nodeFs.writeFileSync / nodeFs.createWriteStream to a destination path follows a symlink an attacker pre-planted there and can leave a half-written file on a crash. The §1 sweep routed every hand-rolled write through an atomic-file primitive: writeSync (buffer payloads — cookie-jar flush, archive/tar extract, backup payloads, p12 export, api-snapshot, sealed-pem marker), writeStream (object-store streaming put — staged into a no-follow exclusive temp + atomic rename, capped by maxBytes), and writeExclSync (the vault seal/unseal/rotate staged write that must re-read + verify the bytes before the rename — clears any stale entry then O_EXCL|O_NOFOLLOW creates so a re-planted symlink fails closed). The three allowlisted files are the genuinely-safe forms: atomic-file.js owns the primitives (its createWriteStream binds an _openExclTemp fd); backup/index.js writeFileSync passes { flag: 'wx' } (O_EXCL refuses a pre-planted target); http-client.js opens its download temp with explicit O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW flags. Any new raw nodeFs.writeFileSync/createWriteStream elsewhere trips this — call the matching atomic-file primitive instead.",
|
|
8511
|
+
},
|
|
8512
|
+
{
|
|
8513
|
+
id: "allowlist-map-indexed-by-untrusted-key-without-hasown",
|
|
8514
|
+
primitive: "Object.prototype.hasOwnProperty.call(MAP, key) — proto-shadow-safe allowlist membership",
|
|
8515
|
+
// Prototype-pollution / proto-shadow class. An object-literal allowlist
|
|
8516
|
+
// (`var MAP = { a: 1 }`) inherits Object.prototype, so MAP["constructor"],
|
|
8517
|
+
// MAP["__proto__"], MAP["toString"], MAP["valueOf"], MAP["hasOwnProperty"]
|
|
8518
|
+
// are all TRUTHY (and !== undefined) even though never added. A membership
|
|
8519
|
+
// gate `if (!MAP[key])` (reject-if-absent) or `if (MAP[key] === undefined)`
|
|
8520
|
+
// is therefore BYPASSED by a proto-named key when `key` is attacker- or
|
|
8521
|
+
// caller-controlled, and `var v = MAP[key]` then hands back a Function /
|
|
8522
|
+
// prototype member. The framework's 312-use idiom is
|
|
8523
|
+
// `Object.prototype.hasOwnProperty.call(MAP, key)` (own-key only). This
|
|
8524
|
+
// detector forbids the bare negated-bracket and `=== undefined` membership
|
|
8525
|
+
// shapes against a SCREAMING_CASE allowlist; reject-if-PRESENT gates
|
|
8526
|
+
// (`if (DROP[tag]) drop`) are a DIFFERENT, fail-safe polarity and use the
|
|
8527
|
+
// positive `if (MAP[key])` form this regex does not match.
|
|
8528
|
+
scanScope: "lib",
|
|
8529
|
+
skipCommentLines: true,
|
|
8530
|
+
regex: /!\s*(?:[a-zA-Z_$][\w$]*\.)?[A-Z][A-Z0-9_]{2,}\[\s*[a-z]|[A-Z][A-Z0-9_]{2,}\[[a-z][\w.]*\]\s*===\s*undefined/,
|
|
8531
|
+
allowlist: [],
|
|
8532
|
+
reason: "Proto-shadow allowlist-bypass class (CWE-1321 prototype pollution / unsafe reflection). A reject-if-absent membership check on an object-literal allowlist — `if (!MAP[key])` or `if (MAP[key] === undefined)` — passes for any Object.prototype member name (constructor / __proto__ / toString / valueOf / hasOwnProperty) when `key` is attacker- or caller-supplied, bypassing the allowlist and (for value-lookup callers) handing a Function downstream. The v0.15.14 sweep converted every such gate across lib/ to the framework's canonical `Object.prototype.hasOwnProperty.call(MAP, key)` membership idiom (already 312 uses). Zero allowlist: a re-introduced `!SCREAMING_MAP[lowercaseKey]` or `SCREAMING_MAP[key] === undefined` membership gate anywhere in lib/ trips this — use hasOwnProperty.call instead. Reject-if-PRESENT gates (`if (DANGEROUS[scheme]) refuse`) are the opposite, fail-safe polarity and are written with the positive `if (MAP[key])` form, which this detector deliberately does not match (adding hasOwnProperty there would weaken them).",
|
|
8533
|
+
},
|
|
8393
8534
|
{
|
|
8394
8535
|
id: "inline-optional-non-empty-string-array-validation",
|
|
8395
8536
|
primitive: "validateOpts.optionalNonEmptyStringArray(value, label, ErrorClass, code?)",
|
|
@@ -231,6 +231,20 @@ function testScreenTypeFilter() {
|
|
|
231
231
|
var r = screener.screen({ name: "Acme Corporation", type: "individual" });
|
|
232
232
|
check("screen type filter: entity matches still allowed",
|
|
233
233
|
r.match === true && r.hits[0].type === "entity");
|
|
234
|
+
|
|
235
|
+
// A sanctions screen MUST over-match on the NAME: a sanctioned INDIVIDUAL
|
|
236
|
+
// screened with a DIFFERING operator-asserted type (the counterparty's
|
|
237
|
+
// unverified self-classification) must STILL match — previously the type
|
|
238
|
+
// filter `continue`d past it, an under-match / fail-open false negative.
|
|
239
|
+
var rv = screener.screen({ name: "JOHN ALEXANDER SMITH", type: "vessel" });
|
|
240
|
+
check("screen: sanctioned individual still matches under a differing type",
|
|
241
|
+
rv.match === true && rv.hits.length >= 1 && rv.hits[0].type === "individual");
|
|
242
|
+
check("screen: type mismatch surfaced as non-dispositive typeMatch:false",
|
|
243
|
+
rv.hits[0].typeMatch === false);
|
|
244
|
+
var rmatch = screener.screen({ name: "JOHN ALEXANDER SMITH", type: "individual" });
|
|
245
|
+
check("screen: typeMatch true when asserted type matches", rmatch.hits[0].typeMatch === true);
|
|
246
|
+
var rnotype = screener.screen({ name: "JOHN ALEXANDER SMITH" });
|
|
247
|
+
check("screen: typeMatch null when no type asserted", rnotype.hits[0].typeMatch === null);
|
|
234
248
|
}
|
|
235
249
|
|
|
236
250
|
function testScreenCorrectAlgorithmEcho() {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.middleware.compression — range/partial-content pass-through.
|
|
4
|
+
*
|
|
5
|
+
* A 206 Partial Content or any Content-Range response must NOT be
|
|
6
|
+
* transformed (RFC 7233 §4.1): compressing it drops Content-Length but
|
|
7
|
+
* leaves Content-Range advertising an uncompressed byte interval over a
|
|
8
|
+
* now-compressed body, corrupting range-assembling clients.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
var helpers = require("../helpers");
|
|
12
|
+
var b = helpers.b;
|
|
13
|
+
var check = helpers.check;
|
|
14
|
+
|
|
15
|
+
function _fakeRes() {
|
|
16
|
+
var headers = {};
|
|
17
|
+
var chunks = [];
|
|
18
|
+
var ended = false;
|
|
19
|
+
return {
|
|
20
|
+
statusCode: 200,
|
|
21
|
+
writeHead: function (status, a, c) {
|
|
22
|
+
this.statusCode = status;
|
|
23
|
+
var hdrs = (c && typeof c === "object") ? c
|
|
24
|
+
: (a && typeof a === "object" && !Array.isArray(a)) ? a : null;
|
|
25
|
+
if (hdrs) {
|
|
26
|
+
var keys = Object.keys(hdrs);
|
|
27
|
+
for (var i = 0; i < keys.length; i++) headers[keys[i].toLowerCase()] = hdrs[keys[i]];
|
|
28
|
+
}
|
|
29
|
+
return this;
|
|
30
|
+
},
|
|
31
|
+
write: function (chunk) {
|
|
32
|
+
if (chunk != null) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
33
|
+
return true;
|
|
34
|
+
},
|
|
35
|
+
end: function (chunk) {
|
|
36
|
+
if (chunk != null) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
37
|
+
ended = true;
|
|
38
|
+
return this;
|
|
39
|
+
},
|
|
40
|
+
setHeader: function (k, v) { headers[k.toLowerCase()] = v; },
|
|
41
|
+
getHeader: function (k) { return headers[k.toLowerCase()]; },
|
|
42
|
+
removeHeader: function (k) { delete headers[k.toLowerCase()]; },
|
|
43
|
+
emit: function () {},
|
|
44
|
+
_headers: headers,
|
|
45
|
+
_body: function () { return Buffer.concat(chunks); },
|
|
46
|
+
_ended: function () { return ended; },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function testPartialContent206NotCompressed() {
|
|
51
|
+
var mw = b.middleware.compression({ threshold: 0 });
|
|
52
|
+
var req = { headers: { "accept-encoding": "gzip" } };
|
|
53
|
+
var res = _fakeRes();
|
|
54
|
+
var body = Buffer.from("X".repeat(4096));
|
|
55
|
+
mw(req, res, function () {});
|
|
56
|
+
res.writeHead(206, {
|
|
57
|
+
"Content-Type": "text/plain",
|
|
58
|
+
"Content-Range": "bytes 0-4095/100000",
|
|
59
|
+
"Content-Length": String(body.length),
|
|
60
|
+
});
|
|
61
|
+
res.end(body);
|
|
62
|
+
check("206 response carries no Content-Encoding", !res.getHeader("content-encoding"));
|
|
63
|
+
check("206 body passes through verbatim (uncompressed)", res._body().equals(body));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function testContentRange200NotCompressed() {
|
|
67
|
+
// A 200 that still carries a Content-Range header (multi-range / explicit
|
|
68
|
+
// single range over 200) must also pass through untransformed.
|
|
69
|
+
var mw = b.middleware.compression({ threshold: 0 });
|
|
70
|
+
var req = { headers: { "accept-encoding": "gzip" } };
|
|
71
|
+
var res = _fakeRes();
|
|
72
|
+
var body = Buffer.from("Y".repeat(4096));
|
|
73
|
+
mw(req, res, function () {});
|
|
74
|
+
res.writeHead(200, {
|
|
75
|
+
"Content-Type": "text/plain",
|
|
76
|
+
"Content-Range": "bytes 0-4095/8192",
|
|
77
|
+
});
|
|
78
|
+
res.end(body);
|
|
79
|
+
check("Content-Range 200 carries no Content-Encoding", !res.getHeader("content-encoding"));
|
|
80
|
+
check("Content-Range 200 body passes through verbatim", res._body().equals(body));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function testPlain200StillCompresses() {
|
|
84
|
+
// Control: a normal compressible 200 still gets compressed, so the
|
|
85
|
+
// range guard isn't disabling compression wholesale.
|
|
86
|
+
var mw = b.middleware.compression({ threshold: 0 });
|
|
87
|
+
var req = { headers: { "accept-encoding": "gzip" } };
|
|
88
|
+
var res = _fakeRes();
|
|
89
|
+
var body = Buffer.from("Z".repeat(4096));
|
|
90
|
+
mw(req, res, function () {});
|
|
91
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
92
|
+
res.end(body);
|
|
93
|
+
check("plain compressible 200 sets Content-Encoding: gzip",
|
|
94
|
+
res.getHeader("content-encoding") === "gzip");
|
|
95
|
+
// zlib flushes asynchronously; wait for the wrapped end before asserting.
|
|
96
|
+
await helpers.waitUntil(function () { return res._ended(); }, {
|
|
97
|
+
timeoutMs: 5000,
|
|
98
|
+
label: "compression-range: compressed response flushed",
|
|
99
|
+
});
|
|
100
|
+
check("plain compressible 200 body is transformed (smaller than source)",
|
|
101
|
+
res._body().length > 0 && res._body().length < body.length);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function run() {
|
|
105
|
+
testPartialContent206NotCompressed();
|
|
106
|
+
testContentRange200NotCompressed();
|
|
107
|
+
await testPlain200StillCompresses();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { run: run };
|
|
111
|
+
|
|
112
|
+
if (require.main === module) {
|
|
113
|
+
run().then(function () { console.log("OK compression-range — " + helpers.getChecks() + " checks"); })
|
|
114
|
+
.catch(function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); });
|
|
115
|
+
}
|
|
@@ -51,7 +51,7 @@ function _makeTsaCert() {
|
|
|
51
51
|
// self-signed (issuer null). Used to assemble a real [leaf, intermediate,
|
|
52
52
|
// root] chain so the identity-assertion chain walk is exercised through
|
|
53
53
|
// an intermediate CA, not only a direct-root / self-signed leaf.
|
|
54
|
-
function _makeCert(cn, issuer) {
|
|
54
|
+
function _makeCert(cn, issuer, isCa) {
|
|
55
55
|
var kp = nodeCrypto.generateKeyPairSync("rsa", { modulusLength: 2048 });
|
|
56
56
|
var spki = kp.publicKey.export({ type: "spki", format: "der" });
|
|
57
57
|
var subjName = _certName(cn);
|
|
@@ -62,7 +62,19 @@ function _makeCert(cn, issuer) {
|
|
|
62
62
|
var serial = asn1.writeInteger(Buffer.from([0x2b]));
|
|
63
63
|
var now = Date.now();
|
|
64
64
|
var validity = asn1.writeSequence([_utcTime(new Date(now - 86400000)), _utcTime(new Date(now + 86400000 * 3650))]);
|
|
65
|
-
var
|
|
65
|
+
var tbsChildren = [version, serial, sigAlgId, issuerName, validity, subjName, spki];
|
|
66
|
+
// Issuer (root / intermediate) certs carry basicConstraints cA:TRUE so the
|
|
67
|
+
// chain walk's cA enforcement (x509Chain.issuerValidlyIssued) accepts them;
|
|
68
|
+
// a leaf omits it (cA:FALSE) as a real end-entity cert does.
|
|
69
|
+
if (isCa) {
|
|
70
|
+
var bcExt = asn1.writeSequence([
|
|
71
|
+
asn1.writeOid("2.5.29.19"), // basicConstraints
|
|
72
|
+
asn1.writeBoolean(true), // critical
|
|
73
|
+
asn1.writeOctetString(asn1.writeSequence([asn1.writeBoolean(true)])), // { cA TRUE }
|
|
74
|
+
]);
|
|
75
|
+
tbsChildren.push(asn1.writeContextExplicit(3, asn1.writeSequence([bcExt])));
|
|
76
|
+
}
|
|
77
|
+
var tbs = asn1.writeSequence(tbsChildren);
|
|
66
78
|
var tbsSig = nodeCrypto.sign("sha256", tbs, issuerKey);
|
|
67
79
|
var certDer = asn1.writeSequence([tbs, sigAlgId, asn1.writeBitString(tbsSig, 0)]);
|
|
68
80
|
return { name: subjName, key: kp.privateKey, pem: new nodeCrypto.X509Certificate(certDer).toString() };
|
|
@@ -321,8 +333,8 @@ async function run() {
|
|
|
321
333
|
// x509 chain through an INTERMEDIATE CA: chain [leaf, intermediate]
|
|
322
334
|
// with the root as the trust anchor must verify — the chain walk uses
|
|
323
335
|
// chain[1..], not only a direct leaf-vs-anchor test.
|
|
324
|
-
var caRoot = _makeCert("CAWG Test Root", null);
|
|
325
|
-
var caInter = _makeCert("CAWG Test Intermediate", { name: caRoot.name, key: caRoot.key });
|
|
336
|
+
var caRoot = _makeCert("CAWG Test Root", null, true);
|
|
337
|
+
var caInter = _makeCert("CAWG Test Intermediate", { name: caRoot.name, key: caRoot.key }, true);
|
|
326
338
|
var caLeaf = _makeCert("Acme Newsroom Leaf", { name: caInter.name, key: caInter.key });
|
|
327
339
|
var iaChain = b.contentCredentials.verifyIdentityAssertion(ia, orgPair.publicKey, {
|
|
328
340
|
referencedAssertions: refs,
|
|
@@ -332,7 +344,7 @@ async function run() {
|
|
|
332
344
|
check("verifyIdentityAssertion: [leaf,intermediate]→root anchor verified (walks intermediates)",
|
|
333
345
|
iaChain.valid === true && iaChain.verified === true);
|
|
334
346
|
// The same chain against an unrelated anchor must NOT verify.
|
|
335
|
-
var caRogue = _makeCert("Rogue Root", null);
|
|
347
|
+
var caRogue = _makeCert("Rogue Root", null, true);
|
|
336
348
|
var iaWrongAnchor = b.contentCredentials.verifyIdentityAssertion(ia, orgPair.publicKey, {
|
|
337
349
|
referencedAssertions: refs,
|
|
338
350
|
identityCertChainPem: [caLeaf.pem, caInter.pem],
|
|
@@ -112,10 +112,11 @@ async function testCorsExplicitSiteOriginRejectsInferredOrigin() {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
async function testCorsXForwardedProtoRespected() {
|
|
115
|
-
// Behind a TLS terminator: socket is HTTP but Origin claims https.
|
|
116
|
-
//
|
|
117
|
-
// scheme so same-origin detection matches
|
|
118
|
-
|
|
115
|
+
// Behind a TLS terminator: socket is HTTP but Origin claims https. With
|
|
116
|
+
// trustedProxies covering the proxy peer, X-Forwarded-Proto: https flips the
|
|
117
|
+
// inferred scheme so same-origin detection matches — peer-gated, so only a
|
|
118
|
+
// request arriving via the trusted proxy gets the header honored.
|
|
119
|
+
var mw = b.middleware.cors({ origins: [], refuseUnknown: true, trustedProxies: ["127.0.0.0/8"] });
|
|
119
120
|
var req = _req({
|
|
120
121
|
method: "POST",
|
|
121
122
|
headers: {
|
|
@@ -126,8 +127,29 @@ async function testCorsXForwardedProtoRespected() {
|
|
|
126
127
|
socket: { remoteAddress: "127.0.0.1", encrypted: false },
|
|
127
128
|
});
|
|
128
129
|
var out = await _drive(mw, req);
|
|
129
|
-
check("X-Forwarded-Proto: https
|
|
130
|
+
check("X-Forwarded-Proto: https via trusted proxy → same-origin pass-through",
|
|
130
131
|
out.nextCalled === true);
|
|
132
|
+
|
|
133
|
+
// A bare trustProxy is refused at construction (spoofable).
|
|
134
|
+
var threwBare = false;
|
|
135
|
+
try { b.middleware.cors({ origins: [], refuseUnknown: true, trustProxy: true }); }
|
|
136
|
+
catch (_e) { threwBare = true; }
|
|
137
|
+
check("cors: bare trustProxy refused (spoofable)", threwBare === true);
|
|
138
|
+
|
|
139
|
+
// Forged X-Forwarded-Proto from an UNTRUSTED peer is ignored → inferred
|
|
140
|
+
// origin stays http://… so the https Origin is cross-origin → blocked.
|
|
141
|
+
var forged = _req({
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: {
|
|
144
|
+
host: "wiki.example.com",
|
|
145
|
+
origin: "https://wiki.example.com",
|
|
146
|
+
"x-forwarded-proto": "https",
|
|
147
|
+
},
|
|
148
|
+
socket: { remoteAddress: "198.51.100.66", encrypted: false },
|
|
149
|
+
});
|
|
150
|
+
var outForged = await _drive(mw, forged);
|
|
151
|
+
check("forged X-Forwarded-Proto from untrusted peer ignored → blocked",
|
|
152
|
+
outForged.nextCalled === false);
|
|
131
153
|
}
|
|
132
154
|
|
|
133
155
|
async function testCorsXForwardedProtoIgnoredWithoutTrustProxy() {
|
|
@@ -169,6 +169,41 @@ async function run() {
|
|
|
169
169
|
var shredded = b.db.from("pr_keyed").where({ _id: "row-3" }).first();
|
|
170
170
|
check("shredded cell reads as absent (null), no crash", shredded.ssn === null && shredded.note === null);
|
|
171
171
|
|
|
172
|
+
// ---- B3a: a SHREDDED read must NOT trip the unseal-failure rate cap ----
|
|
173
|
+
// A crypto-shredded key is an EXPECTED absence (the wrap is gone — no oracle
|
|
174
|
+
// to brute-force), not a forged-ciphertext attack. Bulk-reading erased rows
|
|
175
|
+
// (GDPR eraseHard) must not accrue rate-cap failures, else the cap trips and
|
|
176
|
+
// DoS's the live rows (self-DoS). Configure a low threshold and read the
|
|
177
|
+
// shredded row many times for one (actor, table, column): none must throw
|
|
178
|
+
// crypto-field/unseal-rate-exceeded.
|
|
179
|
+
var rateAudits = [];
|
|
180
|
+
b.cryptoField.configureUnsealRateCap({
|
|
181
|
+
threshold: 2, windowMs: 60000, cooldownMs: 300000,
|
|
182
|
+
onAudit: function (ev) { rateAudits.push(ev); },
|
|
183
|
+
});
|
|
184
|
+
var shredTrip = null;
|
|
185
|
+
for (var sr = 0; sr < 8; sr++) {
|
|
186
|
+
try {
|
|
187
|
+
var srRow = b.db.prepare('SELECT * FROM "pr_keyed" WHERE _id = ?').get("row-3");
|
|
188
|
+
b.cryptoField.unsealRow("pr_keyed", srRow, "shred-reader");
|
|
189
|
+
} catch (e) { if (e.code === "crypto-field/unseal-rate-exceeded") shredTrip = e; }
|
|
190
|
+
}
|
|
191
|
+
check("8 shredded reads do NOT trip the rate cap (shred is exempt)", shredTrip === null);
|
|
192
|
+
check("shredded reads emit no rate-exceeded audit",
|
|
193
|
+
rateAudits.every(function (a) { return a.action !== "system.crypto.unseal_rate_exceeded"; }));
|
|
194
|
+
// CONTROL: a genuine forged-ciphertext read on the same tuple still trips —
|
|
195
|
+
// the exemption is specific to the shredded-key case, not a cap bypass.
|
|
196
|
+
var FORGED_ROW = "vault.aad:Zm9yZ2VkLWdhcmJhZ2U=";
|
|
197
|
+
var forgedTrip = null;
|
|
198
|
+
for (var fr = 0; fr < 8; fr++) {
|
|
199
|
+
try {
|
|
200
|
+
b.cryptoField.unsealRow("pr_keyed", { _id: "row-3", ssn: FORGED_ROW }, "forge-reader");
|
|
201
|
+
} catch (e) { if (e.code === "crypto-field/unseal-rate-exceeded") forgedTrip = e; }
|
|
202
|
+
}
|
|
203
|
+
check("forged-ciphertext reads STILL trip the rate cap (exemption is shred-specific)",
|
|
204
|
+
forgedTrip !== null);
|
|
205
|
+
b.cryptoField.clearRateCapForTest();
|
|
206
|
+
|
|
172
207
|
// ---- FORENSIC RESIDUAL PROOF (the crypto-shred guarantee) ----
|
|
173
208
|
// Advertised (crypto-field.js destroyPerRowKey): destroying the wrapped
|
|
174
209
|
// row-secret makes residual ciphertext (WAL / replica / backup)
|
|
@@ -243,6 +278,9 @@ async function run() {
|
|
|
243
278
|
|
|
244
279
|
// ---- ROTATION ROUND-TRIP ----
|
|
245
280
|
await _rotationRoundTrip();
|
|
281
|
+
|
|
282
|
+
// ---- B3b: crypto-shred under a CUSTOM tablePrefix ----
|
|
283
|
+
await _customPrefixShredLifecycle();
|
|
246
284
|
} finally {
|
|
247
285
|
try { b.cryptoField.clearResidencyForTest(); } catch (_e) {}
|
|
248
286
|
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) {}
|
|
@@ -250,6 +288,51 @@ async function run() {
|
|
|
250
288
|
console.log("OK — crypto-field per-row-key tests");
|
|
251
289
|
}
|
|
252
290
|
|
|
291
|
+
// The per-row-key registry is ALWAYS local (the crypto-shred substrate lives in
|
|
292
|
+
// the local sqlite even in cluster mode). A custom tablePrefix rewrites only the
|
|
293
|
+
// cluster-mode EXTERNAL names; the local table stays raw (_blamejs_per_row_keys).
|
|
294
|
+
// Regression: crypto-field resolved the registry name through the cluster-prefix
|
|
295
|
+
// resolver, so under a custom prefix it targeted a nonexistent <prefix>per_row_keys
|
|
296
|
+
// table — destroyPerRowKey deleted 0 rows and sealed cells stayed decryptable
|
|
297
|
+
// after eraseHard (a SILENT crypto-shred failure). Prove the full lifecycle
|
|
298
|
+
// (seal -> read -> shred -> read-absent) holds under a custom prefix.
|
|
299
|
+
async function _customPrefixShredLifecycle() {
|
|
300
|
+
var dir = fs.mkdtempSync(path.join(os.tmpdir(), "prk-prefix-"));
|
|
301
|
+
try {
|
|
302
|
+
process.env.BLAMEJS_SKIP_NTP_CHECK = "1";
|
|
303
|
+
b.cluster._resetForTest(); b.audit._resetForTest(); b.vault._resetForTest(); b.db._resetForTest();
|
|
304
|
+
helpers.setTestPassphraseEnv ? helpers.setTestPassphraseEnv() : (function () {
|
|
305
|
+
process.env.BLAMEJS_VAULT_PASSPHRASE = "x".repeat(40);
|
|
306
|
+
process.env.BLAMEJS_AUDIT_SIGNING_PASSPHRASE = "x".repeat(40);
|
|
307
|
+
})();
|
|
308
|
+
await b.vault.init({ dataDir: dir });
|
|
309
|
+
await b.db.init({
|
|
310
|
+
dataDir: dir, tmpDir: path.join(dir, "tmpfs"), allowNonTmpfsTmpDir: true,
|
|
311
|
+
tablePrefix: "acme_", // <-- CUSTOM PREFIX
|
|
312
|
+
schema: KEYED_SCHEMA,
|
|
313
|
+
});
|
|
314
|
+
b.cryptoField.clearResidencyForTest();
|
|
315
|
+
b.cryptoField.declarePerRowKey("pr_keyed", { keySize: 32 });
|
|
316
|
+
b.cryptoField.declarePerRowResidency("pr_keyed", {
|
|
317
|
+
residencyColumn: "dataRegion", allowedTags: ["eu", "us", "global"],
|
|
318
|
+
});
|
|
319
|
+
b.db.from("pr_keyed").insertOne({
|
|
320
|
+
_id: "p1", subjectId: "subj-P", dataRegion: "eu", ssn: "123-45-6789", note: "n",
|
|
321
|
+
});
|
|
322
|
+
check("custom-prefix: keyed read round-trips before shred",
|
|
323
|
+
b.db.from("pr_keyed").where({ _id: "p1" }).first().ssn === "123-45-6789");
|
|
324
|
+
var res = b.cryptoField.destroyPerRowKey("pr_keyed", "p1", b.db);
|
|
325
|
+
check("custom-prefix: destroyPerRowKey deletes the wrap (destroyed === 1)", res.destroyed === 1);
|
|
326
|
+
check("custom-prefix: sealed cell is UNREADABLE after shred (crypto-shred holds)",
|
|
327
|
+
b.db.from("pr_keyed").where({ _id: "p1" }).first().ssn === null);
|
|
328
|
+
b.db.close();
|
|
329
|
+
} finally {
|
|
330
|
+
try { b.cryptoField.clearResidencyForTest(); } catch (_e) {}
|
|
331
|
+
try { b.db._resetForTest(); } catch (_e) {}
|
|
332
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_e) {}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
253
336
|
// A vault keypair rotation must reseal the wrapped row-secret in
|
|
254
337
|
// _blamejs_per_row_keys old-root -> new-root (registerTable({aad:true})
|
|
255
338
|
// wires rotate._rotateColumn). After the swap, the wrap unwraps under the
|
|
@@ -137,11 +137,69 @@ async function testFirstOccurrenceWinsForDuplicateCookie() {
|
|
|
137
137
|
r2.outcome === "denied" && r2.status === 403);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
function testMethodsEmptyThrows() {
|
|
141
|
+
// An empty methods array would silently disable the primary CSRF method-gate
|
|
142
|
+
// for all state-changing requests — refuse it at config time.
|
|
143
|
+
var threw = null;
|
|
144
|
+
try { b.middleware.csrfProtect({ cookie: true, methods: [] }); } catch (e) { threw = e; }
|
|
145
|
+
check("csrfProtect({ methods: [] }) refused at config time (no silent CSRF disable)",
|
|
146
|
+
threw && /non-empty array/.test(threw.message || ""));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Two distinct csrf instances on the same response (createApp wires csrf
|
|
150
|
+
// globally AND an operator re-mounts it at the route level) must issue a
|
|
151
|
+
// SINGLE Set-Cookie for the same cookie name — cookie issuance is a
|
|
152
|
+
// response-level resource, deduped by name. Enforcement stays per instance
|
|
153
|
+
// (the next test).
|
|
154
|
+
async function testRedundantMountIssuesSingleCookie() {
|
|
155
|
+
var req = _mockReq({ method: "GET", url: "/", headers: { host: "example.com" } });
|
|
156
|
+
var res = _mockRes();
|
|
157
|
+
var mw1 = b.middleware.csrfProtect({ cookie: true });
|
|
158
|
+
var mw2 = b.middleware.csrfProtect({ cookie: true });
|
|
159
|
+
await new Promise(function (r) { mw1(req, res, r); });
|
|
160
|
+
await new Promise(function (r) { mw2(req, res, r); });
|
|
161
|
+
var sc = res.getHeader("set-cookie");
|
|
162
|
+
var arr = Array.isArray(sc) ? sc : (sc ? [sc] : []);
|
|
163
|
+
var csrfCookies = arr.filter(function (c) { return /^csrf=/.test(c); });
|
|
164
|
+
check("csrf: redundant same-name mount issues a single Set-Cookie",
|
|
165
|
+
csrfCookies.length === 1);
|
|
166
|
+
check("csrf: both instances expose the same req.csrfToken",
|
|
167
|
+
typeof req.csrfToken === "string" && req.csrfToken.length === 64);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The cookie-issuance dedup must NOT disable per-instance enforcement: two
|
|
171
|
+
// distinct instances each validate the double-submit token on a POST. A bad
|
|
172
|
+
// token reaches a deny regardless of which instance runs first — a shared
|
|
173
|
+
// "already handled" flag (the bug the per-instance gate fixed) would let the
|
|
174
|
+
// first instance mark the request handled and skip the second's check.
|
|
175
|
+
async function testDistinctInstancesBothEnforce() {
|
|
176
|
+
var good = b.forms.generateCsrfToken();
|
|
177
|
+
var req = _mockReq({
|
|
178
|
+
method: "POST", url: "/submit",
|
|
179
|
+
headers: { host: "example.com", cookie: "csrf=" + good, "x-csrf-token": "deadbeef" },
|
|
180
|
+
});
|
|
181
|
+
var res = _mockRes();
|
|
182
|
+
var denied = false;
|
|
183
|
+
var origEnd = res.end;
|
|
184
|
+
res.end = function () { denied = true; return origEnd.apply(res, arguments); };
|
|
185
|
+
var mw = b.middleware.csrfProtect({ cookie: true });
|
|
186
|
+
var nextCalled = false;
|
|
187
|
+
await new Promise(function (r) { mw(req, res, function () { nextCalled = true; r(); }); if (denied) r(); });
|
|
188
|
+
// The instance denies the mismatched token (cookie != header), proving
|
|
189
|
+
// enforcement runs per instance and the cookie-dedup did not mark the
|
|
190
|
+
// request "already handled".
|
|
191
|
+
check("csrf: distinct instance enforces double-submit (bad token denied)",
|
|
192
|
+
denied === true && nextCalled === false);
|
|
193
|
+
}
|
|
194
|
+
|
|
140
195
|
async function run() {
|
|
141
196
|
await testSuccessPathDoubleSubmit();
|
|
142
197
|
await testMismatchDenied();
|
|
143
198
|
await testPoisonedCookieNamesDoNotPollute();
|
|
144
199
|
await testFirstOccurrenceWinsForDuplicateCookie();
|
|
200
|
+
await testRedundantMountIssuesSingleCookie();
|
|
201
|
+
await testDistinctInstancesBothEnforce();
|
|
202
|
+
testMethodsEmptyThrows();
|
|
145
203
|
}
|
|
146
204
|
|
|
147
205
|
module.exports = { run: run };
|
|
@@ -41,6 +41,36 @@ function run() {
|
|
|
41
41
|
} catch (e) { threw = /gatekeeper-refused/.test(e.code); }
|
|
42
42
|
check("dataAct: designated gatekeeper share refused (Art 32 §1)", threw);
|
|
43
43
|
|
|
44
|
+
// B10: a gatekeeper as the FINAL domain label must still be refused. The old
|
|
45
|
+
// substring `indexOf(g + ".")` test missed "blog.google" / "data.amazon" (the
|
|
46
|
+
// gatekeeper token is the last label, e.g. the real ".google" TLD), letting an
|
|
47
|
+
// Art 32 §1 share through. Label matching closes the bypass.
|
|
48
|
+
["blog.google", "drive.google", "data.amazon", "play.google.com",
|
|
49
|
+
"https://meta.com/share", "user@booking.com"].forEach(function (recip) {
|
|
50
|
+
var bypassed = false, refused = false;
|
|
51
|
+
try {
|
|
52
|
+
b.dataAct.shareWithThirdParty({
|
|
53
|
+
productId: "thermo-v3", userId: "user-1", recipient: recip, scope: "x",
|
|
54
|
+
});
|
|
55
|
+
bypassed = true; // no throw → the gate let a gatekeeper through
|
|
56
|
+
} catch (e) { refused = /gatekeeper-refused/.test(e.code); }
|
|
57
|
+
check("B10: gatekeeper recipient '" + recip + "' refused (no final-label bypass)",
|
|
58
|
+
refused && !bypassed);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// B10: a NON-gatekeeper whose name merely CONTAINS a gatekeeper token must NOT
|
|
62
|
+
// be over-refused (the substring test false-flagged "notgoogle.com").
|
|
63
|
+
["notgoogle.com", "evilgoogle.com", "google-partners.example.com"].forEach(function (recip) {
|
|
64
|
+
var ok = false;
|
|
65
|
+
try {
|
|
66
|
+
b.dataAct.shareWithThirdParty({
|
|
67
|
+
productId: "thermo-v3", userId: "user-1", recipient: recip, scope: "x",
|
|
68
|
+
});
|
|
69
|
+
ok = true;
|
|
70
|
+
} catch (_e) { ok = false; }
|
|
71
|
+
check("B10: non-gatekeeper '" + recip + "' is NOT over-refused", ok);
|
|
72
|
+
});
|
|
73
|
+
|
|
44
74
|
// Override accepted with audited reason.
|
|
45
75
|
b.dataAct.shareWithThirdParty({
|
|
46
76
|
productId: "thermo-v3", userId: "user-1",
|