@blamejs/core 0.10.15 → 0.11.1
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 +5 -0
- package/index.js +18 -0
- package/lib/auth/oauth.js +187 -0
- package/lib/auth/saml.js +1366 -13
- package/lib/cms-codec.js +141 -0
- package/lib/compliance.js +73 -0
- package/lib/csp.js +271 -0
- package/lib/dbsc.js +299 -0
- package/lib/fedcm.js +264 -0
- package/lib/hal.js +125 -0
- package/lib/http-client.js +46 -10
- package/lib/importmap-integrity.js +90 -0
- package/lib/jsonapi.js +230 -0
- package/lib/lro.js +200 -0
- package/lib/mail-crypto-pgp.js +312 -2
- package/lib/mail-crypto-smime.js +530 -69
- package/lib/metrics.js +62 -12
- package/lib/middleware/security-headers.js +2 -1
- package/lib/ssrf-guard.js +71 -10
- package/lib/standard-webhooks.js +183 -0
- package/lib/web-push-vapid.js +322 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cms-codec.js
CHANGED
|
@@ -674,10 +674,151 @@ function _encryptedContentInfo(plaintext, contentKey) {
|
|
|
674
674
|
]));
|
|
675
675
|
}
|
|
676
676
|
|
|
677
|
+
/**
|
|
678
|
+
* @primitive b.cms.parseSignedData
|
|
679
|
+
* @signature b.cms.parseSignedData(buf, opts?)
|
|
680
|
+
* @since 0.10.16
|
|
681
|
+
* @status stable
|
|
682
|
+
* @related b.cms.encodeSignedData, b.cms.decode
|
|
683
|
+
*
|
|
684
|
+
* Decode a CMS ContentInfo carrying SignedData and walk into the
|
|
685
|
+
* inner structure per RFC 5652 §5.1. Returns a structured object
|
|
686
|
+
* with `digestAlgs`, `encapContent`, `certificates`, and `signerInfos`
|
|
687
|
+
* arrays so downstream verifiers (b.mail.crypto.smime.verify) can
|
|
688
|
+
* check signatures without re-implementing the SignedData walker.
|
|
689
|
+
*
|
|
690
|
+
* @opts
|
|
691
|
+
* maxBytes: number, // default 64 MiB
|
|
692
|
+
*
|
|
693
|
+
* @example
|
|
694
|
+
* var sd = b.cms.parseSignedData(derBytes);
|
|
695
|
+
* sd.signerInfos[0].sigAlgOid; // → "2.16.840.1.101.3.4.3.18" (ML-DSA-65)
|
|
696
|
+
*/
|
|
697
|
+
function parseSignedData(buf, opts) {
|
|
698
|
+
var ci = decode(buf, opts);
|
|
699
|
+
if (ci.contentType !== OID.signedData) {
|
|
700
|
+
throw new CmsCodecError("cms/not-signed-data",
|
|
701
|
+
"parseSignedData: ContentInfo type is " + ci.contentType + ", expected " + OID.signedData);
|
|
702
|
+
}
|
|
703
|
+
if (ci.content.tag !== asn1.TAG.SEQUENCE) {
|
|
704
|
+
throw new CmsCodecError("cms/bad-signed-data", "SignedData must be a SEQUENCE");
|
|
705
|
+
}
|
|
706
|
+
var children = asn1.readSequence(ci.content.value);
|
|
707
|
+
if (children.length < 4) {
|
|
708
|
+
throw new CmsCodecError("cms/bad-signed-data",
|
|
709
|
+
"SignedData SEQUENCE must have at least 4 children");
|
|
710
|
+
}
|
|
711
|
+
var idx = 0;
|
|
712
|
+
idx += 1; // version
|
|
713
|
+
var digestAlgsSet = children[idx]; idx += 1;
|
|
714
|
+
if (digestAlgsSet.tag !== asn1.TAG.SET) {
|
|
715
|
+
throw new CmsCodecError("cms/bad-signed-data", "digestAlgorithms must be a SET");
|
|
716
|
+
}
|
|
717
|
+
var digestAlgs = asn1.readSequence(digestAlgsSet.value).map(_readAlgIdOid);
|
|
718
|
+
var encapInfoNode = children[idx]; idx += 1;
|
|
719
|
+
if (encapInfoNode.tag !== asn1.TAG.SEQUENCE) {
|
|
720
|
+
throw new CmsCodecError("cms/bad-signed-data", "encapContentInfo must be a SEQUENCE");
|
|
721
|
+
}
|
|
722
|
+
var encapContent = _readEncapContent(encapInfoNode);
|
|
723
|
+
var certificates = [];
|
|
724
|
+
while (idx < children.length - 1) {
|
|
725
|
+
var n = children[idx];
|
|
726
|
+
if (n.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && n.tag === 0) {
|
|
727
|
+
var certChildren = asn1.readSequence(n.value);
|
|
728
|
+
for (var ci2 = 0; ci2 < certChildren.length; ci2 += 1) {
|
|
729
|
+
certificates.push(_reEncodeNode(certChildren[ci2]));
|
|
730
|
+
}
|
|
731
|
+
idx += 1;
|
|
732
|
+
} else if (n.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && n.tag === 1) {
|
|
733
|
+
idx += 1;
|
|
734
|
+
} else {
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
var signerInfosSet = children[idx];
|
|
739
|
+
if (!signerInfosSet || signerInfosSet.tag !== asn1.TAG.SET) {
|
|
740
|
+
throw new CmsCodecError("cms/bad-signed-data", "signerInfos must be a SET");
|
|
741
|
+
}
|
|
742
|
+
var signerInfos = asn1.readSequence(signerInfosSet.value).map(_readSignerInfo);
|
|
743
|
+
return {
|
|
744
|
+
digestAlgs: digestAlgs,
|
|
745
|
+
encapContent: encapContent,
|
|
746
|
+
certificates: certificates,
|
|
747
|
+
signerInfos: signerInfos,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function _readAlgIdOid(seqNode) {
|
|
752
|
+
if (seqNode.tag !== asn1.TAG.SEQUENCE) {
|
|
753
|
+
throw new CmsCodecError("cms/bad-alg-id", "AlgorithmIdentifier must be a SEQUENCE");
|
|
754
|
+
}
|
|
755
|
+
var c = asn1.readSequence(seqNode.value);
|
|
756
|
+
if (c.length === 0) {
|
|
757
|
+
throw new CmsCodecError("cms/bad-alg-id", "AlgorithmIdentifier missing OID");
|
|
758
|
+
}
|
|
759
|
+
return asn1.readOid(c[0]);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function _readEncapContent(encapInfoNode) {
|
|
763
|
+
var children = asn1.readSequence(encapInfoNode.value);
|
|
764
|
+
if (children.length === 0) {
|
|
765
|
+
throw new CmsCodecError("cms/bad-encap", "encapContentInfo missing eContentType");
|
|
766
|
+
}
|
|
767
|
+
var eContentType = asn1.readOid(children[0]);
|
|
768
|
+
var eContent = null;
|
|
769
|
+
if (children.length >= 2) {
|
|
770
|
+
var ec = children[1];
|
|
771
|
+
if (ec.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && ec.tag === 0) {
|
|
772
|
+
var inner = asn1.readNode(ec.value);
|
|
773
|
+
if (inner.tag === asn1.TAG.OCTET_STRING) {
|
|
774
|
+
eContent = asn1.readOctetString(inner);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return { eContentType: eContentType, eContent: eContent };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function _readSignerInfo(siNode) {
|
|
782
|
+
if (siNode.tag !== asn1.TAG.SEQUENCE) {
|
|
783
|
+
throw new CmsCodecError("cms/bad-signer-info", "SignerInfo must be a SEQUENCE");
|
|
784
|
+
}
|
|
785
|
+
var c = asn1.readSequence(siNode.value);
|
|
786
|
+
if (c.length < 5) {
|
|
787
|
+
throw new CmsCodecError("cms/bad-signer-info", "SignerInfo must have at least 5 children");
|
|
788
|
+
}
|
|
789
|
+
var idx = 0;
|
|
790
|
+
idx += 1; // version
|
|
791
|
+
var sidNode = c[idx]; idx += 1;
|
|
792
|
+
var digestAlgOid = _readAlgIdOid(c[idx]); idx += 1;
|
|
793
|
+
// Optional [0] IMPLICIT signedAttrs — re-tag as universal SET
|
|
794
|
+
// (0x31) per RFC 5652 §5.4 to recover the byte sequence the
|
|
795
|
+
// signature was computed over.
|
|
796
|
+
var signedAttrsRaw = null;
|
|
797
|
+
if (c[idx] && c[idx].tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && c[idx].tag === 0) {
|
|
798
|
+
var implicitRaw = _reEncodeNode(c[idx]);
|
|
799
|
+
signedAttrsRaw = Buffer.concat([Buffer.from([0x31]), implicitRaw.slice(1)]); // allow:raw-byte-literal — universal SET tag per RFC 5652 §5.4
|
|
800
|
+
idx += 1;
|
|
801
|
+
}
|
|
802
|
+
var sigAlgOid = _readAlgIdOid(c[idx]); idx += 1;
|
|
803
|
+
var sigNode = c[idx]; idx += 1;
|
|
804
|
+
if (sigNode.tag !== asn1.TAG.OCTET_STRING) {
|
|
805
|
+
throw new CmsCodecError("cms/bad-signer-info", "signature must be an OCTET STRING");
|
|
806
|
+
}
|
|
807
|
+
var signature = asn1.readOctetString(sigNode);
|
|
808
|
+
return {
|
|
809
|
+
sid: _reEncodeNode(sidNode),
|
|
810
|
+
digestAlgOid: digestAlgOid,
|
|
811
|
+
signedAttrsRaw: signedAttrsRaw,
|
|
812
|
+
sigAlgOid: sigAlgOid,
|
|
813
|
+
signature: signature,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
677
817
|
module.exports = {
|
|
678
818
|
encodeSignedData: encodeSignedData,
|
|
679
819
|
encodeEnvelopedData: encodeEnvelopedData,
|
|
680
820
|
decode: decode,
|
|
821
|
+
parseSignedData: parseSignedData,
|
|
681
822
|
OID: OID,
|
|
682
823
|
MAX_DEPTH: MAX_DEPTH,
|
|
683
824
|
DEFAULT_MAX_LEN: DEFAULT_MAX_LEN,
|
package/lib/compliance.js
CHANGED
|
@@ -1126,6 +1126,79 @@ var POSTURE_DEFAULTS = Object.freeze({
|
|
|
1126
1126
|
"cmmc-2.0-level-1": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1127
1127
|
"cmmc-2.0-level-2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1128
1128
|
"cmmc-2.0-level-3": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true, fipsMode: false }),
|
|
1129
|
+
// ---- v0.10.16 — sectoral catch-up ----
|
|
1130
|
+
// 42 CFR Part 2 — Substance Use Disorder records confidentiality
|
|
1131
|
+
// (HHS final rule 2024-04-16 aligns Part 2 with HIPAA but retains
|
|
1132
|
+
// a stricter consent floor; encrypted backups + signed audit chain
|
|
1133
|
+
// + post-erase vacuum because the rule narrows the consent window
|
|
1134
|
+
// and operators must demonstrate effective erasure on revocation).
|
|
1135
|
+
"42-cfr-part-2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1136
|
+
// ONC HTI-1 final rule (45 CFR Part 170 / 89 FR 1192, effective
|
|
1137
|
+
// 2024-12-31) — health IT certification. Brings algorithmic
|
|
1138
|
+
// transparency / DSI (Decision Support Interventions) requirements.
|
|
1139
|
+
// Cascade: encrypted backups + signed audit + vacuum (PHI-tier).
|
|
1140
|
+
"hti-1": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1141
|
+
// USCDI v4 (ONC October 2023) — US Core Data for Interoperability
|
|
1142
|
+
// standard data classes for EHR exchange. PHI-tier cascade.
|
|
1143
|
+
"uscdi-v4": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1144
|
+
// IRS Publication 1075 — Federal Tax Information (FTI) safeguards.
|
|
1145
|
+
// FTI-tier: encrypted at rest, signed audit, vacuum after erasure
|
|
1146
|
+
// (Pub 1075 §4.3 requires sanitization on disposal).
|
|
1147
|
+
"irs-1075": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1148
|
+
// NIST 800-172 Rev 3 — Enhanced Security Requirements for Protecting
|
|
1149
|
+
// CUI. Layered atop 800-171 / CMMC-L2. FIPS-validated crypto
|
|
1150
|
+
// floor — same operator-opt-in flag pattern as fedramp-rev5-moderate.
|
|
1151
|
+
"nist-800-172-r3": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true, fipsMode: false }),
|
|
1152
|
+
// FIRST Traffic Light Protocol 2.0 (August 2022) — controls sharing
|
|
1153
|
+
// of cyber threat information. Cascade: signed audit chain (the
|
|
1154
|
+
// protocol's normative effect is on the audit + sharing surface,
|
|
1155
|
+
// not data-at-rest).
|
|
1156
|
+
"tlp-2.0": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1157
|
+
// Security of Critical Infrastructure Act 2018 (Australia, SOCI Act)
|
|
1158
|
+
// + 2021/2022 amendments — critical-infrastructure cyber + ENS
|
|
1159
|
+
// (Enhanced Cyber Security Obligations). Cascade: encrypted backups
|
|
1160
|
+
// + signed audit (ENS §30CT data-integrity obligation).
|
|
1161
|
+
"soci-au": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1162
|
+
// EU NIS 2 Directive (Directive (EU) 2022/2555) — transposition
|
|
1163
|
+
// deadline 2024-10-17. Cybersecurity for essential + important
|
|
1164
|
+
// entities. Encrypted backups + signed audit chain (Art. 21(2)(d)
|
|
1165
|
+
// requires backup management + crisis recovery).
|
|
1166
|
+
"nis2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1167
|
+
// EU Cyber Resilience Act (Reg. (EU) 2024/2847) — product
|
|
1168
|
+
// cybersecurity; full applicability 2027-12-11 with reporting
|
|
1169
|
+
// obligations starting 2026-09-11. SUPPLY-tier cascade.
|
|
1170
|
+
"cra": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1171
|
+
// FFIEC Cybersecurity Assessment Tool 2.0 — financial-tier; aligns
|
|
1172
|
+
// with NIST CSF 2.0 + CRI Profile. Cascade matches glba-safeguards.
|
|
1173
|
+
"ffiec-cat-2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1174
|
+
// CRI Profile v2.0 (Cyber Risk Institute, May 2024) — financial-tier
|
|
1175
|
+
// cyber risk + NIST CSF 2.0 cross-walk.
|
|
1176
|
+
"cri-profile-v2.0": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1177
|
+
// OMB M-22-09 — Moving to Zero Trust (US federal). Cascade: signed
|
|
1178
|
+
// audit + TLS 1.3 (the memorandum's normative effect rides through
|
|
1179
|
+
// the identity + segmentation surfaces).
|
|
1180
|
+
"m-22-09": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1181
|
+
// OMB M-22-18 — Enhancing the Security of the Software Supply Chain
|
|
1182
|
+
// (the SSDF / attestation requirement). SUPPLY-tier — audit-chain
|
|
1183
|
+
// signed for the attestation records.
|
|
1184
|
+
"m-22-18": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1185
|
+
// NIST 800-53 Rev 5 Privacy baseline — additive privacy controls
|
|
1186
|
+
// overlay. Cascade: vacuum-after-erase per PT-2(2) and SI-12.
|
|
1187
|
+
"nist-800-53-r5-privacy": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1188
|
+
// NIST AI-RMF Generative AI Profile (NIST AI 600-1, July 2024) —
|
|
1189
|
+
// generative AI risk management overlay. AI-tier cascade.
|
|
1190
|
+
"nist-ai-600-1-genai": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1191
|
+
// NIST CSF 2.0 (February 2024) — Cybersecurity Framework with the
|
|
1192
|
+
// GOVERN function added.
|
|
1193
|
+
"nist-csf-2.0": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1194
|
+
// SB 53 / California Frontier AI Disclosure (effective 2026 fiscal)
|
|
1195
|
+
// — frontier-model critical incident disclosure ledger.
|
|
1196
|
+
"sb-53": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1197
|
+
// NYC Local Law 144 (2023) — Automated Employment Decision Tools
|
|
1198
|
+
// (bias-audit + candidate notice) — bias-audit posture (already
|
|
1199
|
+
// present as "nyc-ll144"); 2024 amendment adds annual re-audit
|
|
1200
|
+
// signing.
|
|
1201
|
+
"nyc-ll144-2024": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1129
1202
|
});
|
|
1130
1203
|
|
|
1131
1204
|
/**
|
package/lib/csp.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.csp
|
|
4
|
+
* @nav Security
|
|
5
|
+
* @title CSP3 builder
|
|
6
|
+
* @order 150
|
|
7
|
+
* @slug csp
|
|
8
|
+
*
|
|
9
|
+
* @card
|
|
10
|
+
* Content Security Policy Level 3 header builder. Composable per-
|
|
11
|
+
* directive surface with Trusted Types defaults and nonce/hash
|
|
12
|
+
* helpers. Operators wire the resulting header value into
|
|
13
|
+
* b.middleware.securityHeaders via the `csp` opt.
|
|
14
|
+
*
|
|
15
|
+
* @intro
|
|
16
|
+
* Content Security Policy Level 3 (W3C CSP3 / candidate
|
|
17
|
+
* recommendation 2024-09) directive builder. The framework's
|
|
18
|
+
* b.middleware.securityHeaders module ships a strict default CSP;
|
|
19
|
+
* this module exposes the per-directive surface so operators can
|
|
20
|
+
* build out a policy by composition without hand-concatenating
|
|
21
|
+
* strings (which is the failure mode behind most CSP-bypass
|
|
22
|
+
* incidents — a missing `'self'`, an accidental `'unsafe-inline'`,
|
|
23
|
+
* or quoting that the UA silently ignores).
|
|
24
|
+
*
|
|
25
|
+
* Posture:
|
|
26
|
+
* - Refuses `'unsafe-inline'` / `'unsafe-eval'` / `'unsafe-hashes'`
|
|
27
|
+
* in any script-* directive unless explicitly acknowledged via
|
|
28
|
+
* `acknowledgeUnsafe: true` with a documented reason. The CSP3
|
|
29
|
+
* spec defines these as no-ops when `'strict-dynamic'` is
|
|
30
|
+
* present, but UAs that haven't shipped strict-dynamic full
|
|
31
|
+
* support still honor the unsafe keywords — refusing at builder
|
|
32
|
+
* time prevents shipping an unintentional bypass.
|
|
33
|
+
* - Defaults `require-trusted-types-for 'script'` + the named
|
|
34
|
+
* Trusted Types policy "default" when operators wire any
|
|
35
|
+
* script-* source (Trusted Types is the strongest defense
|
|
36
|
+
* against DOM-XSS available in browsers today).
|
|
37
|
+
* - Refuses `data:` in img-src / media-src / font-src unless the
|
|
38
|
+
* operator explicitly opts in (data: URLs sidestep most CSP
|
|
39
|
+
* defenses and are a common XSS pivot).
|
|
40
|
+
* - Refuses `https:` / `*` as a source in any directive (catch-all
|
|
41
|
+
* sources defeat the principle of least privilege).
|
|
42
|
+
*
|
|
43
|
+
* v0.10.16 light-up: builder + nonce helper + hash helper. Trusted
|
|
44
|
+
* Types policy declaration helper. CSP-report-uri / report-to wiring
|
|
45
|
+
* composes with b.middleware.cspReport (existing).
|
|
46
|
+
*
|
|
47
|
+
* Spec citations:
|
|
48
|
+
* - W3C CSP Level 3 (CR 2024-09)
|
|
49
|
+
* - W3C Trusted Types (CR 2023-05)
|
|
50
|
+
* - Reporting API Level 1 (W3C 2024)
|
|
51
|
+
*/
|
|
52
|
+
var nodeCrypto = require("node:crypto");
|
|
53
|
+
var validateOpts = require("./validate-opts");
|
|
54
|
+
var bCrypto = require("./crypto");
|
|
55
|
+
var { defineClass } = require("./framework-error");
|
|
56
|
+
|
|
57
|
+
var CspError = defineClass("CspError", { alwaysPermanent: true });
|
|
58
|
+
|
|
59
|
+
// Directives that participate in script execution — operator-facing
|
|
60
|
+
// keyword discipline only applies here (refuse 'unsafe-*' unless
|
|
61
|
+
// acknowledgeUnsafe is set).
|
|
62
|
+
var SCRIPT_DIRECTIVES = ["script-src", "script-src-elem", "script-src-attr",
|
|
63
|
+
"style-src", "style-src-elem", "style-src-attr",
|
|
64
|
+
"worker-src", "frame-src", "child-src"];
|
|
65
|
+
|
|
66
|
+
var ALL_DIRECTIVES = [
|
|
67
|
+
"default-src", "script-src", "script-src-elem", "script-src-attr",
|
|
68
|
+
"style-src", "style-src-elem", "style-src-attr",
|
|
69
|
+
"img-src", "media-src", "font-src", "connect-src", "object-src",
|
|
70
|
+
"frame-src", "child-src", "worker-src", "manifest-src", "prefetch-src",
|
|
71
|
+
"form-action", "frame-ancestors", "navigate-to", "base-uri", "sandbox",
|
|
72
|
+
"report-to", "report-uri",
|
|
73
|
+
"require-trusted-types-for", "trusted-types",
|
|
74
|
+
"upgrade-insecure-requests", "block-all-mixed-content",
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
var UNSAFE_KEYWORDS = ["'unsafe-inline'", "'unsafe-eval'", "'unsafe-hashes'"];
|
|
78
|
+
var CATCH_ALL_SOURCES = ["*", "https:"];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @primitive b.csp.build
|
|
82
|
+
* @signature b.csp.build(directives, opts?)
|
|
83
|
+
* @since 0.10.16
|
|
84
|
+
* @status stable
|
|
85
|
+
* @compliance soc2, gdpr
|
|
86
|
+
* @related b.middleware.securityHeaders, b.csp.nonce, b.csp.hash
|
|
87
|
+
*
|
|
88
|
+
* Build a CSP3 header value from a per-directive object. Each key is
|
|
89
|
+
* a CSP directive name; each value is an array of sources (strings).
|
|
90
|
+
* Returns a single string ready for `Content-Security-Policy:` or
|
|
91
|
+
* `Content-Security-Policy-Report-Only:`.
|
|
92
|
+
*
|
|
93
|
+
* @opts
|
|
94
|
+
* {
|
|
95
|
+
* acknowledgeUnsafe?: boolean, // default false — refuses 'unsafe-*' otherwise
|
|
96
|
+
* allowDataImages?: boolean, // default false — refuses data: in img-src/media-src/font-src
|
|
97
|
+
* trustedTypesPolicies?: string[], // policy names allowed by trusted-types directive
|
|
98
|
+
* requireTrustedTypes?: boolean, // default true when any script-* is set
|
|
99
|
+
* }
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* var policy = b.csp.build({
|
|
103
|
+
* "default-src": ["'self'"],
|
|
104
|
+
* "script-src": ["'self'", "'nonce-" + req.cspNonce + "'"],
|
|
105
|
+
* "style-src": ["'self'"],
|
|
106
|
+
* "img-src": ["'self'"],
|
|
107
|
+
* "connect-src": ["'self'"],
|
|
108
|
+
* "frame-ancestors": ["'none'"],
|
|
109
|
+
* "base-uri": ["'self'"],
|
|
110
|
+
* "form-action": ["'self'"],
|
|
111
|
+
* "object-src": ["'none'"],
|
|
112
|
+
* "report-to": ["default"],
|
|
113
|
+
* }, { trustedTypesPolicies: ["default", "app-sanitizer"] });
|
|
114
|
+
* res.setHeader("Content-Security-Policy", policy);
|
|
115
|
+
*/
|
|
116
|
+
function build(directives, opts) {
|
|
117
|
+
if (!directives || typeof directives !== "object") {
|
|
118
|
+
throw new CspError("csp/bad-directives",
|
|
119
|
+
"csp.build: directives must be an object keyed by CSP directive name");
|
|
120
|
+
}
|
|
121
|
+
opts = opts || {};
|
|
122
|
+
validateOpts(opts, ["acknowledgeUnsafe", "allowDataImages",
|
|
123
|
+
"trustedTypesPolicies", "requireTrustedTypes"], "csp.build");
|
|
124
|
+
var acknowledgeUnsafe = opts.acknowledgeUnsafe === true;
|
|
125
|
+
var allowDataImages = opts.allowDataImages === true;
|
|
126
|
+
|
|
127
|
+
var keys = Object.keys(directives);
|
|
128
|
+
var hasScriptDirective = false;
|
|
129
|
+
for (var ki = 0; ki < keys.length; ki += 1) {
|
|
130
|
+
var name = keys[ki];
|
|
131
|
+
if (ALL_DIRECTIVES.indexOf(name) === -1) {
|
|
132
|
+
throw new CspError("csp/unknown-directive",
|
|
133
|
+
"csp.build: '" + name + "' is not a recognized CSP3 directive");
|
|
134
|
+
}
|
|
135
|
+
var values = directives[name];
|
|
136
|
+
if (!Array.isArray(values)) {
|
|
137
|
+
throw new CspError("csp/bad-directive-value",
|
|
138
|
+
"csp.build: directives['" + name + "'] must be an array of source strings");
|
|
139
|
+
}
|
|
140
|
+
if (SCRIPT_DIRECTIVES.indexOf(name) !== -1) hasScriptDirective = true;
|
|
141
|
+
for (var vi = 0; vi < values.length; vi += 1) {
|
|
142
|
+
var src = values[vi];
|
|
143
|
+
if (typeof src !== "string" || src.length === 0) {
|
|
144
|
+
throw new CspError("csp/bad-source",
|
|
145
|
+
"csp.build: directives['" + name + "'][" + vi + "] must be a non-empty string");
|
|
146
|
+
}
|
|
147
|
+
// CR/LF/NUL header-injection rejection.
|
|
148
|
+
if (/[\r\n\0]/.test(src)) { // allow:duplicate-regex — CR/LF/NUL header-injection rejection
|
|
149
|
+
throw new CspError("csp/header-injection",
|
|
150
|
+
"csp.build: source '" + src + "' contains CR/LF/NUL");
|
|
151
|
+
}
|
|
152
|
+
if (!acknowledgeUnsafe && SCRIPT_DIRECTIVES.indexOf(name) !== -1 &&
|
|
153
|
+
UNSAFE_KEYWORDS.indexOf(src) !== -1) {
|
|
154
|
+
throw new CspError("csp/unsafe-keyword",
|
|
155
|
+
"csp.build: " + name + " contains " + src + "; pass acknowledgeUnsafe:true with a " +
|
|
156
|
+
"documented justification to allow it (CSP3 §6.2.5.x — unsafe keywords are a " +
|
|
157
|
+
"common XSS bypass surface)");
|
|
158
|
+
}
|
|
159
|
+
if (CATCH_ALL_SOURCES.indexOf(src) !== -1) {
|
|
160
|
+
throw new CspError("csp/catch-all-source",
|
|
161
|
+
"csp.build: " + name + " contains catch-all source '" + src + "'; CSP3 best " +
|
|
162
|
+
"practice refuses these (use an explicit allowlist instead)");
|
|
163
|
+
}
|
|
164
|
+
if (!allowDataImages && (name === "img-src" || name === "media-src" || name === "font-src") &&
|
|
165
|
+
src === "data:") {
|
|
166
|
+
throw new CspError("csp/data-source",
|
|
167
|
+
"csp.build: " + name + " contains 'data:'; pass allowDataImages:true with a " +
|
|
168
|
+
"documented reason (data: URLs sidestep most CSP defenses)");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Trusted Types defaults. When operator has any script-* directive
|
|
174
|
+
// and didn't override `requireTrustedTypes`, append the require-
|
|
175
|
+
// trusted-types-for + trusted-types directives.
|
|
176
|
+
var requireTt = opts.requireTrustedTypes !== false && hasScriptDirective &&
|
|
177
|
+
directives["require-trusted-types-for"] === undefined;
|
|
178
|
+
if (requireTt) {
|
|
179
|
+
directives["require-trusted-types-for"] = ["'script'"];
|
|
180
|
+
}
|
|
181
|
+
if (opts.trustedTypesPolicies && Array.isArray(opts.trustedTypesPolicies) &&
|
|
182
|
+
directives["trusted-types"] === undefined) {
|
|
183
|
+
directives["trusted-types"] = opts.trustedTypesPolicies;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Emit in canonical order (ALL_DIRECTIVES order) so operators
|
|
187
|
+
// diffing two policies see structural diffs rather than ordering
|
|
188
|
+
// noise.
|
|
189
|
+
var out = [];
|
|
190
|
+
for (var di = 0; di < ALL_DIRECTIVES.length; di += 1) {
|
|
191
|
+
var d = ALL_DIRECTIVES[di];
|
|
192
|
+
var vals = directives[d];
|
|
193
|
+
if (vals === undefined) continue;
|
|
194
|
+
if (Array.isArray(vals) && vals.length === 0) {
|
|
195
|
+
// Directives like upgrade-insecure-requests accept no value.
|
|
196
|
+
if (d === "upgrade-insecure-requests" || d === "block-all-mixed-content") {
|
|
197
|
+
out.push(d);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
out.push(d + " " + vals.join(" "));
|
|
203
|
+
}
|
|
204
|
+
return out.join("; ");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @primitive b.csp.nonce
|
|
209
|
+
* @signature b.csp.nonce(byteLen?)
|
|
210
|
+
* @since 0.10.16
|
|
211
|
+
* @status stable
|
|
212
|
+
*
|
|
213
|
+
* Generate a CSP3 nonce — base64url-encoded random bytes for use as
|
|
214
|
+
* `'nonce-<value>'` in script-src / style-src. The CSP3 spec
|
|
215
|
+
* recommends at least 128 bits of entropy (16 bytes); this primitive
|
|
216
|
+
* uses 32 bytes by default for a generous margin.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* req.cspNonce = b.csp.nonce();
|
|
220
|
+
* res.setHeader("Content-Security-Policy",
|
|
221
|
+
* b.csp.build({ "script-src": ["'self'", "'nonce-" + req.cspNonce + "'"] }));
|
|
222
|
+
*/
|
|
223
|
+
function nonce(byteLen) {
|
|
224
|
+
var n = typeof byteLen === "number" ? byteLen : 32; // allow:raw-byte-literal — 256-bit nonce default
|
|
225
|
+
if (!isFinite(n) || n < 16 || n > 64) { // allow:raw-byte-literal — CSP3 §6.2.x nonce bounds
|
|
226
|
+
throw new CspError("csp/bad-nonce-len",
|
|
227
|
+
"csp.nonce: byteLen must be 16-64 (CSP3 §6.2 recommends ≥16 bytes)");
|
|
228
|
+
}
|
|
229
|
+
return bCrypto.toBase64Url(nodeCrypto.randomBytes(n));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @primitive b.csp.hash
|
|
234
|
+
* @signature b.csp.hash(scriptBody, alg?)
|
|
235
|
+
* @since 0.10.16
|
|
236
|
+
* @status stable
|
|
237
|
+
*
|
|
238
|
+
* Compute a CSP3 hash source for an inline script/style. Returns the
|
|
239
|
+
* `'<alg>-<base64>'` token suitable for direct use as a script-src
|
|
240
|
+
* source.
|
|
241
|
+
*
|
|
242
|
+
* @opts
|
|
243
|
+
* alg?: "sha256" | "sha384" | "sha512" // default sha384 (matches
|
|
244
|
+
* // b.crypto.sri default)
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* var src = b.csp.hash("console.log('boot');");
|
|
248
|
+
* // → "'sha384-abcd...'"
|
|
249
|
+
*/
|
|
250
|
+
function hash(scriptBody, alg) {
|
|
251
|
+
if (typeof scriptBody !== "string" && !Buffer.isBuffer(scriptBody)) {
|
|
252
|
+
throw new CspError("csp/bad-hash-input",
|
|
253
|
+
"csp.hash: scriptBody must be a string or Buffer");
|
|
254
|
+
}
|
|
255
|
+
var algName = alg || "sha384";
|
|
256
|
+
if (algName !== "sha256" && algName !== "sha384" && algName !== "sha512") {
|
|
257
|
+
throw new CspError("csp/bad-hash-alg",
|
|
258
|
+
"csp.hash: alg must be sha256 / sha384 / sha512 (CSP3 §6.2 hash sources)");
|
|
259
|
+
}
|
|
260
|
+
var digest = nodeCrypto.createHash(algName)
|
|
261
|
+
.update(typeof scriptBody === "string" ? Buffer.from(scriptBody, "utf8") : scriptBody)
|
|
262
|
+
.digest("base64");
|
|
263
|
+
return "'" + algName + "-" + digest + "'";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = {
|
|
267
|
+
build: build,
|
|
268
|
+
nonce: nonce,
|
|
269
|
+
hash: hash,
|
|
270
|
+
CspError: CspError,
|
|
271
|
+
};
|