@blamejs/core 0.8.52 → 0.8.57
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 +8 -0
- package/lib/audit.js +4 -0
- package/lib/auth/fido-mds3.js +624 -0
- package/lib/auth/passkey.js +214 -2
- package/lib/auth-bot-challenge.js +1 -1
- package/lib/credential-hash.js +2 -2
- package/lib/framework-error.js +55 -0
- package/lib/guard-cidr.js +2 -1
- package/lib/guard-jwt.js +2 -2
- package/lib/guard-oauth.js +2 -2
- package/lib/http-client-cache.js +916 -0
- package/lib/http-client.js +242 -0
- package/lib/mail-arf.js +343 -0
- package/lib/mail-auth.js +265 -40
- package/lib/mail-bimi.js +948 -33
- package/lib/mail-bounce.js +386 -4
- package/lib/mail-mdn.js +424 -0
- package/lib/mail-unsubscribe.js +265 -25
- package/lib/mail.js +403 -21
- package/lib/middleware/bearer-auth.js +1 -1
- package/lib/middleware/clear-site-data.js +122 -0
- package/lib/middleware/dpop.js +1 -1
- package/lib/middleware/index.js +9 -0
- package/lib/middleware/nel.js +214 -0
- package/lib/middleware/security-headers.js +56 -4
- package/lib/middleware/speculation-rules.js +323 -0
- package/lib/mime-parse.js +198 -0
- package/lib/network-dns.js +890 -27
- package/lib/network-tls.js +745 -0
- package/lib/object-store/sigv4.js +54 -0
- package/lib/public-suffix.js +414 -0
- package/lib/safe-buffer.js +7 -0
- package/lib/safe-json.js +1 -1
- package/lib/static.js +120 -0
- package/lib/storage.js +11 -0
- package/lib/vendor/MANIFEST.json +33 -0
- package/lib/vendor/bimi-trust-anchors.pem +33 -0
- package/lib/vendor/public-suffix-list.dat +16376 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.middleware.clearSiteData
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title Clear-Site-Data
|
|
6
|
+
* @order 120
|
|
7
|
+
* @card RFC 9527 Clear-Site-Data middleware — wipe browser-side
|
|
8
|
+
* state (cookies, storage, cache, executionContexts) when
|
|
9
|
+
* a session ends. Mount on logout/erase routes; the
|
|
10
|
+
* header tells the UA to drop everything before navigating
|
|
11
|
+
* away so the next request starts clean.
|
|
12
|
+
*
|
|
13
|
+
* @intro
|
|
14
|
+
* The framework's logout primitive should not just delete the
|
|
15
|
+
* server-side session — it should tell the user-agent to drop every
|
|
16
|
+
* browser-side trace too. RFC 9527 Clear-Site-Data is the header
|
|
17
|
+
* that does it: the UA sees the response and synchronously evicts
|
|
18
|
+
* the named state types BEFORE running any subsequent navigation
|
|
19
|
+
* code, so a stale tab doesn't leak post-logout requests carrying
|
|
20
|
+
* the previous user's cookies.
|
|
21
|
+
*
|
|
22
|
+
* Common shape on a logout endpoint:
|
|
23
|
+
*
|
|
24
|
+
* app.post("/logout", [
|
|
25
|
+
* b.middleware.requireAuth(),
|
|
26
|
+
* async function (req, res) {
|
|
27
|
+
* await req.session.destroy();
|
|
28
|
+
* b.middleware.clearSiteData()(req, res, function () {});
|
|
29
|
+
* res.redirect("/");
|
|
30
|
+
* },
|
|
31
|
+
* ]);
|
|
32
|
+
*
|
|
33
|
+
* Or as drop-in middleware on every route under a path prefix:
|
|
34
|
+
*
|
|
35
|
+
* app.use("/account/erase", b.middleware.clearSiteData());
|
|
36
|
+
*
|
|
37
|
+
* Default types: `cookies`, `storage`, `cache`, `executionContexts`.
|
|
38
|
+
* Operators wanting a narrower wipe (e.g. only `cache`) pass
|
|
39
|
+
* `{ types: ["cache"] }`. Wildcard `"*"` is supported but discouraged
|
|
40
|
+
* — it tells the UA to wipe the whole origin including cross-tab
|
|
41
|
+
* service workers, which often surprises operators.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
var validateOpts = require("../validate-opts");
|
|
45
|
+
|
|
46
|
+
// RFC 9527 §3 — the canonical token set. `clientHints` was added in
|
|
47
|
+
// the 2024 revision; `executionContexts` reloads any documents the
|
|
48
|
+
// origin currently has open (closes XSS-style hijacked tabs).
|
|
49
|
+
var KNOWN_TYPES = {
|
|
50
|
+
"cookies": true,
|
|
51
|
+
"storage": true,
|
|
52
|
+
"cache": true,
|
|
53
|
+
"executionContexts": true,
|
|
54
|
+
"clientHints": true,
|
|
55
|
+
"*": true,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
var DEFAULT_TYPES = ["cookies", "storage", "cache", "executionContexts"];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @primitive b.middleware.clearSiteData
|
|
62
|
+
* @signature b.middleware.clearSiteData(req, res, next)
|
|
63
|
+
* @since 0.8.53
|
|
64
|
+
* @status stable
|
|
65
|
+
* @related b.middleware.securityHeaders, b.session
|
|
66
|
+
*
|
|
67
|
+
* Builds middleware that emits an RFC 9527 Clear-Site-Data response
|
|
68
|
+
* header. Mount on logout / account-erase / consent-revoke routes
|
|
69
|
+
* so the user-agent wipes browser-side state synchronously before
|
|
70
|
+
* the next navigation. Without this header, a logged-out tab can
|
|
71
|
+
* still carry cookies and cached responses past the server-side
|
|
72
|
+
* session destruction, leaking post-logout requests.
|
|
73
|
+
*
|
|
74
|
+
* @opts
|
|
75
|
+
* {
|
|
76
|
+
* types: Array<"cookies"|"storage"|"cache"|"executionContexts"|"clientHints"|"*">,
|
|
77
|
+
* }
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* var b = require("@blamejs/core");
|
|
81
|
+
* var app = b.router.create();
|
|
82
|
+
* app.post("/logout", [
|
|
83
|
+
* b.middleware.clearSiteData(),
|
|
84
|
+
* async function (req, res) {
|
|
85
|
+
* await req.session.destroy();
|
|
86
|
+
* res.redirect("/");
|
|
87
|
+
* },
|
|
88
|
+
* ]);
|
|
89
|
+
*/
|
|
90
|
+
function create(opts) {
|
|
91
|
+
opts = opts || {};
|
|
92
|
+
validateOpts(opts, ["types"], "middleware.clearSiteData");
|
|
93
|
+
var types = opts.types === undefined ? DEFAULT_TYPES : opts.types;
|
|
94
|
+
if (!Array.isArray(types) || types.length === 0) {
|
|
95
|
+
throw new TypeError("middleware.clearSiteData: opts.types must be a non-empty array");
|
|
96
|
+
}
|
|
97
|
+
for (var i = 0; i < types.length; i += 1) {
|
|
98
|
+
var t = types[i];
|
|
99
|
+
if (typeof t !== "string" || !KNOWN_TYPES[t]) {
|
|
100
|
+
throw new TypeError(
|
|
101
|
+
"middleware.clearSiteData: unknown type '" + t +
|
|
102
|
+
"' (expected one of: " + Object.keys(KNOWN_TYPES).join(", ") + ")");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Header value is a comma-separated list of double-quoted tokens
|
|
106
|
+
// per RFC 9527 §3 (Structured Field Value List of Strings). Build
|
|
107
|
+
// once at construction time — runtime cost is one setHeader call.
|
|
108
|
+
var headerValue = types.map(function (t) { return '"' + t + '"'; }).join(", ");
|
|
109
|
+
|
|
110
|
+
return function clearSiteData(req, res, next) {
|
|
111
|
+
if (typeof res.setHeader === "function") {
|
|
112
|
+
res.setHeader("Clear-Site-Data", headerValue);
|
|
113
|
+
}
|
|
114
|
+
next();
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
create: create,
|
|
120
|
+
KNOWN_TYPES: Object.keys(KNOWN_TYPES),
|
|
121
|
+
DEFAULT_TYPES: DEFAULT_TYPES,
|
|
122
|
+
};
|
package/lib/middleware/dpop.js
CHANGED
|
@@ -121,7 +121,7 @@ function _reconstructHtu(req) {
|
|
|
121
121
|
* @primitive b.middleware.dpop
|
|
122
122
|
* @signature b.middleware.dpop(opts)
|
|
123
123
|
* @since 0.1.0
|
|
124
|
-
* @related b.middleware.bearerAuth
|
|
124
|
+
* @related b.middleware.bearerAuth
|
|
125
125
|
*
|
|
126
126
|
* RFC 9449 Demonstrating Proof of Possession (DPoP). Verifies the
|
|
127
127
|
* `DPoP` header on inbound requests, attaches `req.dpop = { header,
|
package/lib/middleware/index.js
CHANGED
|
@@ -25,6 +25,7 @@ var assetlinks = require("./assetlinks");
|
|
|
25
25
|
var attachUser = require("./attach-user");
|
|
26
26
|
var bearerAuth = require("./bearer-auth");
|
|
27
27
|
var bodyParser = require("./body-parser");
|
|
28
|
+
var clearSiteData = require("./clear-site-data");
|
|
28
29
|
var botDisclose = require("./bot-disclose");
|
|
29
30
|
var botGuard = require("./bot-guard");
|
|
30
31
|
var compression = require("./compression");
|
|
@@ -42,8 +43,10 @@ var gpc = require("./gpc");
|
|
|
42
43
|
var headers = require("./headers");
|
|
43
44
|
var health = require("./health");
|
|
44
45
|
var hostAllowlist = require("./host-allowlist");
|
|
46
|
+
var nel = require("./nel");
|
|
45
47
|
var networkAllowlist = require("./network-allowlist");
|
|
46
48
|
var rateLimit = require("./rate-limit");
|
|
49
|
+
var speculationRules = require("./speculation-rules");
|
|
47
50
|
var requestId = require("./request-id");
|
|
48
51
|
var requestLog = require("./request-log");
|
|
49
52
|
var requireAal = require("./require-aal");
|
|
@@ -108,6 +111,9 @@ module.exports = {
|
|
|
108
111
|
tracePropagate: tracePropagate.create,
|
|
109
112
|
tusUpload: tusUpload.create,
|
|
110
113
|
webAppManifest: webAppManifest.create,
|
|
114
|
+
clearSiteData: clearSiteData.create,
|
|
115
|
+
nel: nel.create,
|
|
116
|
+
speculationRules: speculationRules.create,
|
|
111
117
|
|
|
112
118
|
// Module exports for advanced use (constants, raw factory access)
|
|
113
119
|
_modules: {
|
|
@@ -152,6 +158,9 @@ module.exports = {
|
|
|
152
158
|
tracePropagate: tracePropagate,
|
|
153
159
|
tusUpload: tusUpload,
|
|
154
160
|
webAppManifest: webAppManifest,
|
|
161
|
+
clearSiteData: clearSiteData,
|
|
162
|
+
nel: nel,
|
|
163
|
+
speculationRules: speculationRules,
|
|
155
164
|
},
|
|
156
165
|
};
|
|
157
166
|
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.middleware.nel
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title Network Error Logging
|
|
6
|
+
* @order 125
|
|
7
|
+
* @card W3C Network Error Logging — emits the `NEL` and companion
|
|
8
|
+
* `Report-To` headers so user-agents post network-failure
|
|
9
|
+
* reports (DNS failures, TLS handshake errors, TCP resets,
|
|
10
|
+
* HTTP-error class samples) back to an operator-controlled
|
|
11
|
+
* collector. Pair with `b.middleware.cspReport` for a
|
|
12
|
+
* unified browser-side telemetry channel.
|
|
13
|
+
*
|
|
14
|
+
* @intro
|
|
15
|
+
* Network Error Logging (W3C draft) is the browser's native channel
|
|
16
|
+
* for surfacing failures the server never sees: TLS handshake
|
|
17
|
+
* collapse before the request body, DNS lookup misses, CDN routing
|
|
18
|
+
* resets, premature TCP teardown mid-response. The user-agent
|
|
19
|
+
* buffers these and POSTs JSON reports to a configured collector,
|
|
20
|
+
* keyed by the `report-to` group named in the `NEL` header.
|
|
21
|
+
*
|
|
22
|
+
* The middleware emits two response headers on every request it
|
|
23
|
+
* sees:
|
|
24
|
+
*
|
|
25
|
+
* Report-To: { "group": "default", "max_age": 86400, "endpoints":
|
|
26
|
+
* [ { "url": "https://collector.example.com/nel" } ] }
|
|
27
|
+
* NEL: { "report_to": "default", "max_age": 86400,
|
|
28
|
+
* "include_subdomains": false, "success_fraction": 0,
|
|
29
|
+
* "failure_fraction": 1 }
|
|
30
|
+
*
|
|
31
|
+
* Both header values are JSON dictionaries; the framework refuses
|
|
32
|
+
* any operator-supplied collector URL containing CR/LF/NUL so a
|
|
33
|
+
* typo can't smuggle a header-injection payload into the wire
|
|
34
|
+
* format.
|
|
35
|
+
*
|
|
36
|
+
* Mount AFTER `securityHeaders` (so the response writeHead order
|
|
37
|
+
* stays predictable) and BEFORE business middleware. Pair with
|
|
38
|
+
* `b.middleware.cspReport` so a single collector receives both NEL
|
|
39
|
+
* and CSP reports — operators commonly point both at the same
|
|
40
|
+
* `/_telemetry` endpoint.
|
|
41
|
+
*
|
|
42
|
+
* app.use(b.middleware.requestId());
|
|
43
|
+
* app.use(b.middleware.securityHeaders());
|
|
44
|
+
* app.use(b.middleware.nel({
|
|
45
|
+
* reportTo: "default",
|
|
46
|
+
* collectorUrl: "https://collector.example.com/nel",
|
|
47
|
+
* maxAge: 86400,
|
|
48
|
+
* includeSubdomains: false,
|
|
49
|
+
* successFraction: 0,
|
|
50
|
+
* failureFraction: 1,
|
|
51
|
+
* }));
|
|
52
|
+
*
|
|
53
|
+
* The `successFraction` defaults to 0 because reporting every
|
|
54
|
+
* successful request is a billing surprise on busy origins;
|
|
55
|
+
* operators tune it up (0.001, 0.01) when sampling success
|
|
56
|
+
* distribution intentionally.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
var validateOpts = require("../validate-opts");
|
|
60
|
+
var C = require("../constants");
|
|
61
|
+
|
|
62
|
+
// Per W3C draft + the practical browser implementations. successFraction
|
|
63
|
+
// = 1.0 reports every request — fine for a low-traffic admin surface,
|
|
64
|
+
// catastrophic on a high-traffic CDN. failureFraction = 1.0 is the
|
|
65
|
+
// security-correct default; operators only lower it when they have a
|
|
66
|
+
// downstream rate-limit on the collector.
|
|
67
|
+
var DEFAULT_REPORT_GROUP = "default";
|
|
68
|
+
var DEFAULT_MAX_AGE = C.TIME.hours(24) / C.TIME.seconds(1); // NEL header takes seconds
|
|
69
|
+
var DEFAULT_SUCCESS_FRACTION = 0;
|
|
70
|
+
var DEFAULT_FAILURE_FRACTION = 1;
|
|
71
|
+
|
|
72
|
+
// Header injection defense — every operator-supplied string that
|
|
73
|
+
// reaches a header value is screened for CR/LF/NUL. The collector
|
|
74
|
+
// URL flows into JSON inside Report-To; a CR there would let an
|
|
75
|
+
// attacker forge an arbitrary follow-up header on stacks that
|
|
76
|
+
// concatenate header lines naively.
|
|
77
|
+
var INJECTION_RE = /[\r\n\0]/;
|
|
78
|
+
|
|
79
|
+
function _refuseInjection(value, label) {
|
|
80
|
+
if (typeof value !== "string") return;
|
|
81
|
+
if (INJECTION_RE.test(value)) { // allow:regex-no-length-cap — CR/LF/NUL injection check, length bounded by caller
|
|
82
|
+
throw new TypeError(
|
|
83
|
+
"middleware.nel: " + label + " contains CR/LF/NUL — refused as a " +
|
|
84
|
+
"header-injection vector");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @primitive b.middleware.nel
|
|
90
|
+
* @signature b.middleware.nel(req, res, next)
|
|
91
|
+
* @since 0.8.53
|
|
92
|
+
* @status stable
|
|
93
|
+
* @related b.middleware.cspReport, b.middleware.securityHeaders
|
|
94
|
+
*
|
|
95
|
+
* Builds middleware that emits the W3C Network Error Logging `NEL`
|
|
96
|
+
* and companion `Report-To` headers so user-agents post failure
|
|
97
|
+
* telemetry back to an operator-controlled collector. Mount near the
|
|
98
|
+
* top of the chain (after `requestId` and `securityHeaders`) so every
|
|
99
|
+
* response carries the headers — NEL is a long-lived browser policy,
|
|
100
|
+
* not a per-route concern.
|
|
101
|
+
*
|
|
102
|
+
* The two header bodies are JSON dictionaries built once at construct
|
|
103
|
+
* time. Operator-supplied strings flow through a CR/LF/NUL refusal
|
|
104
|
+
* check so a typo in `collectorUrl` can't smuggle additional headers
|
|
105
|
+
* onto the wire.
|
|
106
|
+
*
|
|
107
|
+
* @opts
|
|
108
|
+
* {
|
|
109
|
+
* reportTo: string, // group name (default "default")
|
|
110
|
+
* collectorUrl: string, // required — collector POST URL
|
|
111
|
+
* maxAge: number, // policy lifetime in seconds (default 86400)
|
|
112
|
+
* includeSubdomains: boolean, // default false
|
|
113
|
+
* successFraction: number, // 0..1, default 0
|
|
114
|
+
* failureFraction: number, // 0..1, default 1
|
|
115
|
+
* }
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* var b = require("@blamejs/core");
|
|
119
|
+
* var app = b.router.create();
|
|
120
|
+
* app.use(b.middleware.requestId());
|
|
121
|
+
* app.use(b.middleware.securityHeaders());
|
|
122
|
+
* app.use(b.middleware.nel({
|
|
123
|
+
* collectorUrl: "https://collector.example.com/nel",
|
|
124
|
+
* maxAge: 86400,
|
|
125
|
+
* successFraction: 0,
|
|
126
|
+
* failureFraction: 1,
|
|
127
|
+
* }));
|
|
128
|
+
*/
|
|
129
|
+
function create(opts) {
|
|
130
|
+
opts = opts || {};
|
|
131
|
+
validateOpts(opts, [
|
|
132
|
+
"reportTo", "collectorUrl", "maxAge",
|
|
133
|
+
"includeSubdomains", "successFraction", "failureFraction",
|
|
134
|
+
], "middleware.nel");
|
|
135
|
+
|
|
136
|
+
// Per-file allowlist in test/layer-0-primitives/codebase-patterns.test.js
|
|
137
|
+
// for inline-require-non-empty-string-validation — the operator-readable
|
|
138
|
+
// "collectorUrl is required" prose is part of the public test contract;
|
|
139
|
+
// validateOpts.requireNonEmptyString would emit a generic
|
|
140
|
+
// "validate-opts/missing-non-empty-string" message instead.
|
|
141
|
+
if (typeof opts.collectorUrl !== "string" || opts.collectorUrl.length === 0) {
|
|
142
|
+
throw new TypeError(
|
|
143
|
+
"middleware.nel: opts.collectorUrl is required (the URL the user-agent " +
|
|
144
|
+
"POSTs network-failure reports to)");
|
|
145
|
+
}
|
|
146
|
+
_refuseInjection(opts.collectorUrl, "opts.collectorUrl");
|
|
147
|
+
|
|
148
|
+
// The collector URL must be an https:// scheme — browsers only
|
|
149
|
+
// honor secure-origin report endpoints. Refusing at config-time so
|
|
150
|
+
// an operator typo (`http://`) surfaces at boot, not as silent
|
|
151
|
+
// never-fires-in-production.
|
|
152
|
+
if (opts.collectorUrl.slice(0, 8) !== "https://") { // allow:raw-byte-literal — string-prefix length, not bytes
|
|
153
|
+
throw new TypeError(
|
|
154
|
+
"middleware.nel: opts.collectorUrl must be https:// (browsers " +
|
|
155
|
+
"ignore non-secure NEL collectors); got " + opts.collectorUrl);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
var reportTo = opts.reportTo === undefined ? DEFAULT_REPORT_GROUP : opts.reportTo;
|
|
159
|
+
if (typeof reportTo !== "string" || reportTo.length === 0) {
|
|
160
|
+
throw new TypeError("middleware.nel: opts.reportTo must be a non-empty string");
|
|
161
|
+
}
|
|
162
|
+
_refuseInjection(reportTo, "opts.reportTo");
|
|
163
|
+
|
|
164
|
+
var maxAge = opts.maxAge === undefined ? DEFAULT_MAX_AGE : opts.maxAge;
|
|
165
|
+
if (typeof maxAge !== "number" || !isFinite(maxAge) || maxAge < 0) {
|
|
166
|
+
throw new TypeError("middleware.nel: opts.maxAge must be a non-negative finite number (seconds)");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
var includeSubdomains = opts.includeSubdomains === undefined ? false : !!opts.includeSubdomains;
|
|
170
|
+
|
|
171
|
+
var successFraction = opts.successFraction === undefined ? DEFAULT_SUCCESS_FRACTION : opts.successFraction;
|
|
172
|
+
if (typeof successFraction !== "number" || !isFinite(successFraction) ||
|
|
173
|
+
successFraction < 0 || successFraction > 1) {
|
|
174
|
+
throw new TypeError("middleware.nel: opts.successFraction must be a number in [0, 1]");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
var failureFraction = opts.failureFraction === undefined ? DEFAULT_FAILURE_FRACTION : opts.failureFraction;
|
|
178
|
+
if (typeof failureFraction !== "number" || !isFinite(failureFraction) ||
|
|
179
|
+
failureFraction < 0 || failureFraction > 1) {
|
|
180
|
+
throw new TypeError("middleware.nel: opts.failureFraction must be a number in [0, 1]");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Build the two header values once at construct time. JSON.stringify
|
|
184
|
+
// produces the canonical compact form; the property ordering is
|
|
185
|
+
// stable per V8 spec.
|
|
186
|
+
var reportToHeader = JSON.stringify({
|
|
187
|
+
group: reportTo,
|
|
188
|
+
max_age: maxAge,
|
|
189
|
+
endpoints: [{ url: opts.collectorUrl }],
|
|
190
|
+
});
|
|
191
|
+
var nelHeader = JSON.stringify({
|
|
192
|
+
report_to: reportTo,
|
|
193
|
+
max_age: maxAge,
|
|
194
|
+
include_subdomains: includeSubdomains,
|
|
195
|
+
success_fraction: successFraction,
|
|
196
|
+
failure_fraction: failureFraction,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return function nel(req, res, next) {
|
|
200
|
+
if (typeof res.setHeader === "function") {
|
|
201
|
+
res.setHeader("Report-To", reportToHeader);
|
|
202
|
+
res.setHeader("NEL", nelHeader);
|
|
203
|
+
}
|
|
204
|
+
next();
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
create: create,
|
|
210
|
+
DEFAULT_REPORT_GROUP: DEFAULT_REPORT_GROUP,
|
|
211
|
+
DEFAULT_MAX_AGE: DEFAULT_MAX_AGE,
|
|
212
|
+
DEFAULT_SUCCESS_FRACTION: DEFAULT_SUCCESS_FRACTION,
|
|
213
|
+
DEFAULT_FAILURE_FRACTION: DEFAULT_FAILURE_FRACTION,
|
|
214
|
+
};
|
|
@@ -76,12 +76,51 @@ var DEFAULT_CSP =
|
|
|
76
76
|
"font-src 'self'; " +
|
|
77
77
|
"connect-src 'self'; " +
|
|
78
78
|
"frame-ancestors 'none'; " +
|
|
79
|
+
// CSP3 fenced-frame-src: refuse <fencedframe> embeds entirely. The
|
|
80
|
+
// Privacy-Sandbox-era element bypasses traditional frame controls;
|
|
81
|
+
// operators wanting to embed a Privacy-Sandbox vendor opt in by
|
|
82
|
+
// passing their own csp.
|
|
83
|
+
"fenced-frame-src 'none'; " +
|
|
79
84
|
"base-uri 'self'; " +
|
|
80
85
|
"form-action 'self'; " +
|
|
81
86
|
"object-src 'none'; " +
|
|
82
87
|
"require-trusted-types-for 'script'; " +
|
|
83
88
|
"trusted-types 'allow-duplicates' default;";
|
|
84
89
|
|
|
90
|
+
// Document-Policy default — denies the highest-risk DOM/JS surfaces
|
|
91
|
+
// that aren't otherwise covered by Permissions-Policy. `unsized-media`
|
|
92
|
+
// blocks layout-jank from images without explicit width/height,
|
|
93
|
+
// `oversized-images` caps the served-vs-displayed ratio, and the
|
|
94
|
+
// document-write feature disables the legacy synchronous DOM-injection
|
|
95
|
+
// API. Operators with a third-party widget that needs the legacy API
|
|
96
|
+
// override.
|
|
97
|
+
var DEFAULT_DOCUMENT_POLICY =
|
|
98
|
+
"document-write=?0, " +
|
|
99
|
+
"unsized-media=?0, " +
|
|
100
|
+
"oversized-images=?0";
|
|
101
|
+
|
|
102
|
+
// RFC 9651 (Structured Field Values) Permissions-Policy validation —
|
|
103
|
+
// each policy is `feature=value-list` where value-list is `*` /
|
|
104
|
+
// `(self)` / `(self "https://...")` / `()` (empty = deny). Reject
|
|
105
|
+
// header values that don't conform; operators get a clear refusal at
|
|
106
|
+
// boot rather than a silently-broken header at runtime.
|
|
107
|
+
var PP_POLICY_RE =
|
|
108
|
+
/^[a-z][a-z0-9-]*=(?:\*|\([^)]*\)|self)$/;
|
|
109
|
+
function _validatePermissionsPolicy(value) {
|
|
110
|
+
if (typeof value !== "string" || value.length === 0) return;
|
|
111
|
+
var parts = String(value).split(/\s*,\s*/);
|
|
112
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
113
|
+
var p = parts[i];
|
|
114
|
+
if (!p) continue;
|
|
115
|
+
if (!PP_POLICY_RE.test(p)) { // allow:regex-no-length-cap — RFC 9651 SF entries are bounded by browser parsers; operator-supplied
|
|
116
|
+
throw new TypeError(
|
|
117
|
+
"middleware.securityHeaders: permissionsPolicy entry '" + p +
|
|
118
|
+
"' is not a valid RFC 9651 structured field (expected " +
|
|
119
|
+
"'feature=*' / 'feature=()' / 'feature=(self ...)')");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
85
124
|
/**
|
|
86
125
|
* @primitive b.middleware.securityHeaders
|
|
87
126
|
* @signature b.middleware.securityHeaders(req, res, next)
|
|
@@ -115,6 +154,9 @@ var DEFAULT_CSP =
|
|
|
115
154
|
* originAgentCluster: "?1"|"?0"|false,
|
|
116
155
|
* dnsPrefetchControl: "off"|"on"|false,
|
|
117
156
|
* csp: string|false,
|
|
157
|
+
* documentPolicy: string|false,
|
|
158
|
+
* acceptCh: string|false,
|
|
159
|
+
* criticalCh: string|false,
|
|
118
160
|
* reportingEndpoints: object,
|
|
119
161
|
* trustProxy: boolean|number,
|
|
120
162
|
* }
|
|
@@ -132,8 +174,11 @@ function create(opts) {
|
|
|
132
174
|
"hsts", "contentTypeOptions", "frameOptions", "referrerPolicy",
|
|
133
175
|
"permissionsPolicy", "coop", "coep", "corp",
|
|
134
176
|
"originAgentCluster", "dnsPrefetchControl", "csp", "trustProxy",
|
|
135
|
-
"reportingEndpoints",
|
|
177
|
+
"reportingEndpoints", "documentPolicy", "criticalCh", "acceptCh",
|
|
136
178
|
], "middleware.securityHeaders");
|
|
179
|
+
if (opts.permissionsPolicy && typeof opts.permissionsPolicy === "string") {
|
|
180
|
+
_validatePermissionsPolicy(opts.permissionsPolicy);
|
|
181
|
+
}
|
|
137
182
|
var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
|
|
138
183
|
? opts.trustProxy : false;
|
|
139
184
|
var hsts = opts.hsts === undefined ? "max-age=63072000; includeSubDomains; preload" : opts.hsts;
|
|
@@ -147,6 +192,9 @@ function create(opts) {
|
|
|
147
192
|
var oac = opts.originAgentCluster === undefined ? "?1" : opts.originAgentCluster;
|
|
148
193
|
var dpc = opts.dnsPrefetchControl === undefined ? "off" : opts.dnsPrefetchControl;
|
|
149
194
|
var csp = opts.csp === undefined ? DEFAULT_CSP : opts.csp;
|
|
195
|
+
var docPolicy = opts.documentPolicy === undefined ? DEFAULT_DOCUMENT_POLICY : opts.documentPolicy;
|
|
196
|
+
var criticalCh = opts.criticalCh && typeof opts.criticalCh === "string" ? opts.criticalCh : false;
|
|
197
|
+
var acceptCh = opts.acceptCh && typeof opts.acceptCh === "string" ? opts.acceptCh : false;
|
|
150
198
|
// Reporting-Endpoints (W3C Reporting API) — when operator passes a
|
|
151
199
|
// map of endpoint-name → URL, we emit `Reporting-Endpoints: name="url",
|
|
152
200
|
// name2="url2", ...` and (when default CSP is in force) append
|
|
@@ -194,13 +242,17 @@ function create(opts) {
|
|
|
194
242
|
if (oac) res.setHeader("Origin-Agent-Cluster", oac);
|
|
195
243
|
if (dpc) res.setHeader("X-DNS-Prefetch-Control", dpc);
|
|
196
244
|
if (csp) res.setHeader("Content-Security-Policy", csp);
|
|
245
|
+
if (docPolicy) res.setHeader("Document-Policy", docPolicy);
|
|
246
|
+
if (acceptCh) res.setHeader("Accept-CH", acceptCh);
|
|
247
|
+
if (criticalCh) res.setHeader("Critical-CH", criticalCh);
|
|
197
248
|
if (reportingEndpoints) res.setHeader("Reporting-Endpoints", reportingEndpoints);
|
|
198
249
|
next();
|
|
199
250
|
};
|
|
200
251
|
}
|
|
201
252
|
|
|
202
253
|
module.exports = {
|
|
203
|
-
create:
|
|
204
|
-
DEFAULT_PERMISSIONS:
|
|
205
|
-
DEFAULT_CSP:
|
|
254
|
+
create: create,
|
|
255
|
+
DEFAULT_PERMISSIONS: DEFAULT_PERMISSIONS,
|
|
256
|
+
DEFAULT_CSP: DEFAULT_CSP,
|
|
257
|
+
DEFAULT_DOCUMENT_POLICY: DEFAULT_DOCUMENT_POLICY,
|
|
206
258
|
};
|