@alxia/secure-headers 0.4.9 → 0.5.0
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/README.md +108 -2
- package/dist/clear-site-data.d.ts +49 -0
- package/dist/clear-site-data.d.ts.map +1 -0
- package/dist/csp.d.ts +36 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +191 -13
- package/dist/index.js.map +7 -4
- package/dist/page.d.ts +58 -0
- package/dist/page.d.ts.map +1 -0
- package/dist/secure-headers.d.ts +5 -0
- package/dist/secure-headers.d.ts.map +1 -1
- package/docs/README.md +1 -1
- package/docs/guide.md +252 -5
- package/docs/roadmap.md +26 -6
- package/docs/troubleshooting.md +334 -12
- package/package.json +3 -3
package/docs/troubleshooting.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Each entry is headed by the text you see: a TypeScript error, an exception,
|
|
4
4
|
a browser console message, or a header that is or is not on a response.
|
|
5
|
-
`secureHeaders`
|
|
6
|
-
|
|
5
|
+
`secureHeaders`, `csp()`, `pageHeaders()` and `clearSiteData()` throw only
|
|
6
|
+
at startup, on options they cannot send; `cacheControl` is an option of
|
|
7
|
+
`secureHeaders`. What goes wrong otherwise is a type, a browser refusing what a header forbids,
|
|
7
8
|
or a header in the wrong place.
|
|
8
9
|
|
|
9
10
|
**Types**
|
|
@@ -14,14 +15,29 @@ or a header in the wrong place.
|
|
|
14
15
|
- [`Type 'undefined' is not assignable to type 'SecureHeadersOptions & { readonly nonce: true; }'`](#type-undefined-is-not-assignable-to-type-secureheadersoptions---readonly-nonce-true-)
|
|
15
16
|
- [`Property 'nonce' does not exist on type 'RouteBase<…>'`](#property-nonce-does-not-exist-on-type-routebase)
|
|
16
17
|
- [`No overload matches this call` … `Type 'boolean' is not assignable to type 'true'`](#no-overload-matches-this-call--type-boolean-is-not-assignable-to-type-true)
|
|
18
|
+
- [`Type 'string' is not assignable to type 'CspValue | undefined'`](#type-string-is-not-assignable-to-type-cspvalue--undefined)
|
|
19
|
+
- [`Object literal may only specify known properties, and 'contentSecurityPolicy' does not exist in type 'PageHeadersOptions'`](#object-literal-may-only-specify-known-properties-and-contentsecuritypolicy-does-not-exist-in-type-pageheadersoptions)
|
|
20
|
+
- [`Property 'https' is missing in type '{}' but required in type 'PageHeadersOptions'`](#property-https-is-missing-in-type--but-required-in-type-pageheadersoptions)
|
|
21
|
+
- [`Type '"cash"' is not assignable to type '"cookies" | "*" | "cache" | "storage" | "executionContexts" | "clientHints" | "prefetchCache" | "prerenderCache"'`](#type-cash-is-not-assignable-to-type-cookies----cache--storage--executioncontexts--clienthints--prefetchcache--prerendercache)
|
|
17
22
|
|
|
18
23
|
**Runtime**
|
|
19
24
|
|
|
20
25
|
- [`TypeError: secureHeaders: … is empty; give false to leave the … header out`](#typeerror-secureheaders--is-empty-give-false-to-leave-the--header-out)
|
|
21
|
-
- [`use(): argument 1 looks like a factory (
|
|
26
|
+
- [`use(): argument 1 looks like a factory (<name>): call it, use(<name>())`](#use-argument-1-looks-like-a-factory-name-call-it-usename)
|
|
22
27
|
- [`TypeError: secureHeaders: nonce is on, but the content-security-policy has no script-src to add it to: …`](#typeerror-secureheaders-nonce-is-on-but-the-content-security-policy-has-no-script-src-to-add-it-to-)
|
|
23
28
|
- [`TypeError: secureHeaders: nonce is on, but contentSecurityPolicy is false: …`](#typeerror-secureheaders-nonce-is-on-but-contentsecuritypolicy-is-false-)
|
|
24
29
|
- [`TypeError: secureHeaders: contentSecurityPolicy names NONCE, but nonce is off: give nonce: true`](#typeerror-secureheaders-contentsecuritypolicy-names-nonce-but-nonce-is-off-give-nonce-true)
|
|
30
|
+
- [`TypeError: csp(): <name> has no source: write ["'none'"] to allow nothing, or false to leave it out`](#typeerror-csp-name-has-no-source-write-none-to-allow-nothing-or-false-to-leave-it-out)
|
|
31
|
+
- [`TypeError: csp(): "<name>" is not a directive name: write lowercase letters, digits and hyphens, starting with a letter, as in "script-src"`](#typeerror-csp-name-is-not-a-directive-name-write-lowercase-letters-digits-and-hyphens-starting-with-a-letter-as-in-script-src)
|
|
32
|
+
- [`TypeError: csp(): <name> has the source "<source>", which is empty or holds ";", "," or a space: a source is one token, so split a list into several sources, and write a comma inside a URL as %2C`](#typeerror-csp-name-has-the-source-source-which-is-empty-or-holds---or-a-space-a-source-is-one-token-so-split-a-list-into-several-sources-and-write-a-comma-inside-a-url-as-2c)
|
|
33
|
+
- [`TypeError: csp(): no directive is left: give at least one, such as { "default-src": ["'none'"] }`](#typeerror-csp-no-directive-is-left-give-at-least-one-such-as--default-src-none-)
|
|
34
|
+
- [`TypeError: clearSiteData(): no type is given: write at least one, such as ["cache"]`](#typeerror-clearsitedata-no-type-is-given-write-at-least-one-such-as-cache)
|
|
35
|
+
- [`TypeError: clearSiteData(): "<type>" is not a type: write one of "cache", "cookies", "storage", "executionContexts", "clientHints", "prefetchCache", "prerenderCache", "*"`](#typeerror-clearsitedata-type-is-not-a-type-write-one-of-cache-cookies-storage-executioncontexts-clienthints-prefetchcache-prerendercache-)
|
|
36
|
+
- [`TypeError: pageHeaders(): script-src has no NONCE: a page script runs only with the response's nonce, so write script-src with NONCE in directives, such as ["'self'", NONCE, "'strict-dynamic'"]`](#typeerror-pageheaders-script-src-has-no-nonce-a-page-script-runs-only-with-the-responses-nonce-so-write-script-src-with-nonce-in-directives-such-as-self-nonce-strict-dynamic)
|
|
37
|
+
- [`TypeError: pageHeaders(): script-src-elem has no NONCE: script elements follow it instead of script-src, so add NONCE to it, or give false to remove it`](#typeerror-pageheaders-script-src-elem-has-no-nonce-script-elements-follow-it-instead-of-script-src-so-add-nonce-to-it-or-give-false-to-remove-it)
|
|
38
|
+
- [`TypeError: pageHeaders(): <name> cannot be removed from a page policy: …`](#typeerror-pageheaders-name-cannot-be-removed-from-a-page-policy-)
|
|
39
|
+
- [`TypeError: csp(): <value> in <directive> is a keyword: write it quoted, '<value>'`](#typeerror-csp-value-in-directive-is-a-keyword-write-it-quoted-value)
|
|
40
|
+
- [`TypeError: clearSiteData(): methods is empty or holds an empty method: write at least one, such as ["POST"], or leave methods out`](#typeerror-clearsitedata-methods-is-empty-or-holds-an-empty-method-write-at-least-one-such-as-post-or-leave-methods-out)
|
|
25
41
|
|
|
26
42
|
**In the browser**
|
|
27
43
|
|
|
@@ -29,6 +45,7 @@ or a header in the wrong place.
|
|
|
29
45
|
- [`Refused to send form data to '…' because it violates the following Content Security Policy directive: "form-action 'none'"`](#refused-to-send-form-data-to--because-it-violates-the-following-content-security-policy-directive-form-action-none)
|
|
30
46
|
- [`Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-…'"`](#refused-to-execute-inline-script-because-it-violates-the-following-content-security-policy-directive-script-src-self-nonce-)
|
|
31
47
|
- [Inline style attributes stop applying once `style-src` has the nonce](#inline-style-attributes-stop-applying-once-style-src-has-the-nonce)
|
|
48
|
+
- [A third-party script or font is refused by a page policy](#a-third-party-script-or-font-is-refused-by-a-page-policy)
|
|
32
49
|
- [`Refused to frame '…' because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'none'"`](#refused-to-frame--because-an-ancestor-violates-the-following-content-security-policy-directive-frame-ancestors-none)
|
|
33
50
|
- [`net::ERR_BLOCKED_BY_RESPONSE.NotSameOrigin`](#neterr_blocked_by_responsenotsameorigin)
|
|
34
51
|
- [`window.opener` is `null` in a sign-in popup](#windowopener-is-null-in-a-sign-in-popup)
|
|
@@ -37,11 +54,13 @@ or a header in the wrong place.
|
|
|
37
54
|
**Headers**
|
|
38
55
|
|
|
39
56
|
- [An option has no effect on a response](#an-option-has-no-effect-on-a-response)
|
|
57
|
+
- [A CDN still caches a page, though `cacheControl` is set](#a-cdn-still-caches-a-page-though-cachecontrol-is-set)
|
|
40
58
|
- [`X-Powered-By` or `Server` is still sent](#x-powered-by-or-server-is-still-sent)
|
|
41
59
|
- [A response arrives without the headers](#a-response-arrives-without-the-headers)
|
|
42
60
|
- [A 404 or a route outside a group has no headers](#a-404-or-a-route-outside-a-group-has-no-headers)
|
|
43
61
|
- [A `try`/`catch` middleware's reply has no headers](#a-trycatch-middlewares-reply-has-no-headers)
|
|
44
62
|
- [A route has no headers, though the others do](#a-route-has-no-headers-though-the-others-do)
|
|
63
|
+
- [`Clear-Site-Data` is missing on a client-side sign-out](#clear-site-data-is-missing-on-a-client-side-sign-out)
|
|
45
64
|
|
|
46
65
|
## Types
|
|
47
66
|
|
|
@@ -65,8 +84,9 @@ place of `'true'`.
|
|
|
65
84
|
|
|
66
85
|
**Why:** a header option is the header's value as a string, or `false` to
|
|
67
86
|
leave it out (`Setting`, exported, is `string | false`). There is no
|
|
68
|
-
`true` — leaving the key out is the default
|
|
69
|
-
|
|
87
|
+
`true` — leaving the key out is the default. The option takes a string,
|
|
88
|
+
and `csp()` builds that string from directives
|
|
89
|
+
([guide](guide.md#writing-the-policy-as-directives)).
|
|
70
90
|
|
|
71
91
|
**Fix:** write the header as it goes out, or leave the key out:
|
|
72
92
|
|
|
@@ -216,6 +236,75 @@ const app = useNonce
|
|
|
216
236
|
|
|
217
237
|
Or keep it on everywhere: the nonce costs one random draw per request.
|
|
218
238
|
|
|
239
|
+
### `Type 'string' is not assignable to type 'CspValue | undefined'`
|
|
240
|
+
|
|
241
|
+
**When:** a directive of `csp()`, or of `pageHeaders({ directives })`, is given a bare string instead of a list.
|
|
242
|
+
|
|
243
|
+
```text
|
|
244
|
+
error TS2322: Type 'string' is not assignable to type 'CspValue | undefined'.
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Why:** a directive's value is a list of sources (`readonly string[]`), `true` for a bare directive, or `false` to leave it out. One string would be one source, and a space inside it is refused at startup.
|
|
248
|
+
|
|
249
|
+
**Fix:** write a list, even for one source:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
csp({ 'script-src': ["'self'"] });
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### `Object literal may only specify known properties, and 'contentSecurityPolicy' does not exist in type 'PageHeadersOptions'`
|
|
256
|
+
|
|
257
|
+
**When:** `pageHeaders()` is given `contentSecurityPolicy`. `'nonce'` gives the same message, with the name changed.
|
|
258
|
+
|
|
259
|
+
```text
|
|
260
|
+
error TS2353: Object literal may only specify known properties, and 'contentSecurityPolicy' does not exist in type 'PageHeadersOptions'.
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Why:** a page policy is written through `directives`, merged over `PAGE_DIRECTIVES`, and always carries a nonce, so neither `contentSecurityPolicy` nor `nonce` is an option of `pageHeaders()`.
|
|
264
|
+
|
|
265
|
+
**Fix:** use `directives`, and drop `nonce`:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
pageHeaders({ https: true, directives: { 'img-src': ["'self'", 'https://img.example'] } });
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
For a whole policy of your own, use `secureHeaders({ nonce: true, contentSecurityPolicy: csp(…) })`.
|
|
272
|
+
|
|
273
|
+
### `Property 'https' is missing in type '{}' but required in type 'PageHeadersOptions'`
|
|
274
|
+
|
|
275
|
+
**When:** `pageHeaders({})`, or an options object without `https`.
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
error TS2345: Argument of type '{}' is not assignable to parameter of type 'PageHeadersOptions'.
|
|
279
|
+
Property 'https' is missing in type '{}' but required in type 'PageHeadersOptions'.
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Why:** `https` switches HSTS and `upgrade-insecure-requests`, and the package never reads the environment to guess it.
|
|
283
|
+
|
|
284
|
+
**Fix:** pass your own flag:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
pageHeaders({ https: import.meta.env.PROD });
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### `Type '"cash"' is not assignable to type '"cookies" | "*" | "cache" | "storage" | "executionContexts" | "clientHints" | "prefetchCache" | "prerenderCache"'`
|
|
291
|
+
|
|
292
|
+
**When:** `clearSiteData(['cash'])`, a type the header does not know, or a type of `ClearSiteDataType` misspelled.
|
|
293
|
+
|
|
294
|
+
```text
|
|
295
|
+
error TS2322: Type '"cash"' is not assignable to type '"cookies" | "*" | "cache" | "storage" | "executionContexts" | "clientHints" | "prefetchCache" | "prerenderCache"'.
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The union is `ClearSiteDataType`, written out.
|
|
299
|
+
|
|
300
|
+
**Why:** the browser ignores a type it does not know, so the cache would stay as it was without a word.
|
|
301
|
+
|
|
302
|
+
**Fix:** use one of the listed types, in a list:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
clearSiteData(['cache']);
|
|
306
|
+
```
|
|
307
|
+
|
|
219
308
|
## Runtime
|
|
220
309
|
|
|
221
310
|
### `TypeError: secureHeaders: … is empty; give false to leave the … header out`
|
|
@@ -237,20 +326,21 @@ Only `false` leaves a header out.
|
|
|
237
326
|
app.use(secureHeaders({ xFrameOptions: false }));
|
|
238
327
|
```
|
|
239
328
|
|
|
240
|
-
### `use(): argument 1 looks like a factory (
|
|
329
|
+
### `use(): argument 1 looks like a factory (<name>): call it, use(<name>())`
|
|
241
330
|
|
|
242
|
-
**When:** `alxia().use(secureHeaders)`,
|
|
243
|
-
|
|
331
|
+
**When:** `alxia().use(secureHeaders)`, `use(pageHeaders)` or
|
|
332
|
+
`use(clearSiteData)`, a factory given uncalled, in JavaScript or past a
|
|
333
|
+
cast. `<name>` is the factory's. It throws where the app is declared, since
|
|
244
334
|
alxia 0.5; before, each request was answered with a 500 and the log said
|
|
245
335
|
`GET /: a middleware (secureHeaders) returned function: …`. TypeScript
|
|
246
336
|
refuses it first, with `"this looks like a factory given uncalled: …"` in
|
|
247
337
|
the error.
|
|
248
338
|
|
|
249
|
-
**Why:** `use` takes a middleware, a `(ctx, next)` function; `secureHeaders
|
|
250
|
-
|
|
251
|
-
refuse
|
|
339
|
+
**Why:** `use` takes a middleware, a `(ctx, next)` function; `secureHeaders`,
|
|
340
|
+
`pageHeaders` and `clearSiteData` make one, and are marked as factories, so
|
|
341
|
+
`use`, a route and `plugin` refuse them.
|
|
252
342
|
|
|
253
|
-
**Fix:**
|
|
343
|
+
**Fix:** call it:
|
|
254
344
|
|
|
255
345
|
```ts
|
|
256
346
|
const app = alxia()
|
|
@@ -317,6 +407,174 @@ import { NONCE, secureHeaders } from '@alxia/secure-headers';
|
|
|
317
407
|
app.use(secureHeaders({ nonce: true, contentSecurityPolicy: `script-src 'self' ${NONCE}` }));
|
|
318
408
|
```
|
|
319
409
|
|
|
410
|
+
### `TypeError: csp(): <name> has no source: write ["'none'"] to allow nothing, or false to leave it out`
|
|
411
|
+
|
|
412
|
+
**When:** `csp()` is given an empty array for a directive. It throws at once, so the app fails at startup:
|
|
413
|
+
|
|
414
|
+
```text
|
|
415
|
+
TypeError: csp(): <name> has no source: write ["'none'"] to allow nothing, or false to leave it out
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**Why:** an empty list says nothing a browser can use, and omitting the directive is not the same as forbidding everything.
|
|
419
|
+
|
|
420
|
+
**Fix:**
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
csp({ 'img-src': ["'none'"] }); // allow nothing
|
|
424
|
+
csp({ 'img-src': false }); // leave the directive out
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### `TypeError: csp(): "<name>" is not a directive name: write lowercase letters, digits and hyphens, starting with a letter, as in "script-src"`
|
|
428
|
+
|
|
429
|
+
**When:** a key of the object is not lowercase letters, digits and hyphens starting with a letter, such as `scriptSrc`, `Script-Src` or `-x`. It throws at once, so the app fails at startup:
|
|
430
|
+
|
|
431
|
+
```text
|
|
432
|
+
TypeError: csp(): "<name>" is not a directive name: write lowercase letters, digits and hyphens, starting with a letter, as in "script-src"
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Why:** directive names are written as in the header; a camel-cased key would name a directive no browser knows, so it is refused, not sent.
|
|
436
|
+
|
|
437
|
+
**Fix:**
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
csp({ 'script-src': ["'self'"] });
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### `TypeError: csp(): <name> has the source "<source>", which is empty or holds ";", "," or a space: a source is one token, so split a list into several sources, and write a comma inside a URL as %2C`
|
|
444
|
+
|
|
445
|
+
**When:** a source is an empty string, or holds `;`, `,` or whitespace, usually a whole list or a second directive in one string. It throws at once, so the app fails at startup:
|
|
446
|
+
|
|
447
|
+
```text
|
|
448
|
+
TypeError: csp(): <name> has the source "<source>", which is empty or holds ";", "," or a space: a source is one token, so split a list into several sources, and write a comma inside a URL as %2C
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Why:** a `;` would end the directive and start another, so a value could smuggle in a directive; a space would pass two sources as one; a `,` would split the header in two for a proxy. A comma inside a URL is legitimate, so percent-encode it as `%2C`.
|
|
452
|
+
|
|
453
|
+
**Fix:**
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
csp({ 'img-src': ["'self'", 'data:'] }); // not ["'self' data:"]
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### `TypeError: csp(): no directive is left: give at least one, such as { "default-src": ["'none'"] }`
|
|
460
|
+
|
|
461
|
+
**When:** every directive is `false` or `undefined`, or the object is empty. It throws at once, so the app fails at startup:
|
|
462
|
+
|
|
463
|
+
```text
|
|
464
|
+
TypeError: csp(): no directive is left: give at least one, such as { "default-src": ["'none'"] }
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Why:** an empty policy is no policy; it would be sent as an empty header.
|
|
468
|
+
|
|
469
|
+
**Fix:**
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
csp({ 'default-src': ["'none'"] });
|
|
473
|
+
// or leave the header out: secureHeaders({ contentSecurityPolicy: false })
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### `TypeError: clearSiteData(): no type is given: write at least one, such as ["cache"]`
|
|
477
|
+
|
|
478
|
+
**When:** `clearSiteData([])`, called with no type, throws where the app is declared.
|
|
479
|
+
|
|
480
|
+
**Why:** a `Clear-Site-Data` header with no value tells the browser nothing.
|
|
481
|
+
|
|
482
|
+
**Fix:** name what to clear:
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
app.use('/sign-out', clearSiteData(['cache']));
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### `TypeError: clearSiteData(): "<type>" is not a type: write one of "cache", "cookies", "storage", "executionContexts", "clientHints", "prefetchCache", "prerenderCache", "*"`
|
|
489
|
+
|
|
490
|
+
**When:** a type is misspelled (`'cash'`) or is not one the header knows, past a cast or in JavaScript. TypeScript refuses it first.
|
|
491
|
+
|
|
492
|
+
**Why:** the browser ignores a type it does not know, so the cache would stay as it was without a word.
|
|
493
|
+
|
|
494
|
+
**Fix:** use one of the types the message lists:
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
app.use('/sign-out', clearSiteData(['cache', 'cookies']));
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### `TypeError: pageHeaders(): script-src has no NONCE: a page script runs only with the response's nonce, so write script-src with NONCE in directives, such as ["'self'", NONCE, "'strict-dynamic'"]`
|
|
501
|
+
|
|
502
|
+
**When:** `pageHeaders({ directives: { 'script-src': [...] } })` gives a `script-src` without `NONCE`, or `false`, and the app is declared.
|
|
503
|
+
|
|
504
|
+
**Why:** a page script runs only with the response's nonce. A `script-src` that lacks `NONCE` would leave every script of the page refused, and the page would not hydrate.
|
|
505
|
+
|
|
506
|
+
**Fix:** keep `NONCE` in the list you give, with `'strict-dynamic'` after it:
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
import { NONCE, pageHeaders } from '@alxia/secure-headers';
|
|
510
|
+
|
|
511
|
+
pageHeaders({
|
|
512
|
+
https: true,
|
|
513
|
+
directives: { 'script-src': ["'self'", NONCE, "'strict-dynamic'"] },
|
|
514
|
+
});
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### `TypeError: pageHeaders(): script-src-elem has no NONCE: script elements follow it instead of script-src, so add NONCE to it, or give false to remove it`
|
|
518
|
+
|
|
519
|
+
**When:** `pageHeaders({ directives: { 'script-src-elem': [...] } })` gives a `script-src-elem` list without `NONCE`.
|
|
520
|
+
|
|
521
|
+
**Why:** browsers that support CSP level 3 check `<script>` elements against `script-src-elem` when it is present, and ignore `script-src` for them. A list without `NONCE` would leave every script of the page refused, and the page would not hydrate.
|
|
522
|
+
|
|
523
|
+
**Fix:** add `NONCE` to the list, or give `false` to remove the directive so `script-src` governs again:
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
import { NONCE, pageHeaders } from '@alxia/secure-headers';
|
|
527
|
+
|
|
528
|
+
pageHeaders({
|
|
529
|
+
https: true,
|
|
530
|
+
directives: { 'script-src-elem': ["'self'", NONCE, "'strict-dynamic'"] },
|
|
531
|
+
});
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### `TypeError: pageHeaders(): <name> cannot be removed from a page policy: …`
|
|
535
|
+
|
|
536
|
+
**When:** `pageHeaders({ directives: { 'default-src': false } })`, or `false` or `true` for `base-uri` or `object-src`. The app fails where it is declared.
|
|
537
|
+
|
|
538
|
+
```text
|
|
539
|
+
TypeError: pageHeaders(): base-uri cannot be removed from a page policy: it has no fallback, so an injected <base> would redirect nonced relative scripts, so give it a list of sources
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
**Why:** `default-src` covers frame, media and child sources, and `base-uri` has no fallback, so without it an injected `<base>` would redirect the nonced relative scripts. `object-src` blocks plugin content.
|
|
543
|
+
|
|
544
|
+
**Fix:** give the directive a list, or leave it out of `directives` to keep the preset's:
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
pageHeaders({ https: true, directives: { 'base-uri': ["'self'"] } });
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### `TypeError: csp(): <value> in <directive> is a keyword: write it quoted, '<value>'`
|
|
551
|
+
|
|
552
|
+
**When:** a source of `csp()` is `self`, `none`, `unsafe-inline`, `unsafe-eval`, `unsafe-hashes`, `strict-dynamic`, `wasm-unsafe-eval`, `report-sample` or `inline-speculation-rules`, or starts `nonce-`, `sha256-`, `sha384-` or `sha512-`, written without its single quotes (in any case). The same goes for `pageHeaders({ directives })`.
|
|
553
|
+
|
|
554
|
+
```text
|
|
555
|
+
TypeError: csp(): self in script-src is a keyword: write it quoted, 'self'
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**Why:** a browser reads `self` as a host called `self`, not as this site, and ignores the keyword it meant. The policy would be looser or stricter than written, without a word.
|
|
559
|
+
|
|
560
|
+
**Fix:** quote it inside the string:
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
csp({ 'script-src': ["'self'", NONCE] });
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### `TypeError: clearSiteData(): methods is empty or holds an empty method: write at least one, such as ["POST"], or leave methods out`
|
|
567
|
+
|
|
568
|
+
**When:** `clearSiteData(['cache'], { methods: [] })`, or a `methods` list that holds `''`. It throws where the app is declared.
|
|
569
|
+
|
|
570
|
+
**Why:** with no method, the middleware would never send the header, and the sign-out would leave the cache as it was.
|
|
571
|
+
|
|
572
|
+
**Fix:** name the methods, or leave `methods` out for `['POST']`:
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
clearSiteData(['cache'], { methods: ['POST', 'DELETE'] });
|
|
576
|
+
```
|
|
577
|
+
|
|
320
578
|
## In the browser
|
|
321
579
|
|
|
322
580
|
These messages are Chrome's wording; Firefox and Safari report the same
|
|
@@ -409,6 +667,26 @@ directive of their own:
|
|
|
409
667
|
contentSecurityPolicy: `default-src 'self'; script-src 'self'; style-src-elem 'self' ${NONCE}; style-src-attr 'unsafe-inline'`,
|
|
410
668
|
```
|
|
411
669
|
|
|
670
|
+
### A third-party script or font is refused by a page policy
|
|
671
|
+
|
|
672
|
+
**When:** a page behind `pageHeaders()` loads a script, a stylesheet or a font from another origin, and the console reads `Refused to load the script 'https://…' because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-…' 'strict-dynamic'"` (or `font-src 'self'`).
|
|
673
|
+
|
|
674
|
+
**Why:** `PAGE_DIRECTIVES` allows this site only. A script another script adds is allowed through `'strict-dynamic'`; one named in the HTML needs the nonce, and a font needs its origin in `font-src`.
|
|
675
|
+
|
|
676
|
+
**Fix:** extend the directive that loads it through `directives`, which replaces that directive's list, so repeat what it already holds:
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
pageHeaders({
|
|
680
|
+
https: true,
|
|
681
|
+
directives: {
|
|
682
|
+
'font-src': ["'self'", 'https://fonts.example'],
|
|
683
|
+
'style-src-elem': ["'self'", NONCE, 'https://fonts.example'],
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
Never put the origin in `default-src`. Under `'strict-dynamic'` a third-party script is loaded by a nonced script, not by allowlisting its host: a script tag from another origin takes the page's `nonce` attribute, and a host in `script-src` is ignored by browsers that support `'strict-dynamic'`. To allow a host for images, extend `img-src` the same way, as in `directives: { 'img-src': ["'self'", 'https://img.example'] }`.
|
|
689
|
+
|
|
412
690
|
### `Refused to frame '…' because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'none'"`
|
|
413
691
|
|
|
414
692
|
**When:** a page puts a response of the app in an `<iframe>`. With the
|
|
@@ -501,6 +779,34 @@ const response = await app.request('/the-route');
|
|
|
501
779
|
console.log(response.headers.get('content-security-policy'));
|
|
502
780
|
```
|
|
503
781
|
|
|
782
|
+
### A CDN still caches a page, though `cacheControl` is set
|
|
783
|
+
|
|
784
|
+
**When:** `cacheControl: 'private, no-store'` is set, and some pages are still
|
|
785
|
+
stored by a cache.
|
|
786
|
+
|
|
787
|
+
**Why:** a route that sets its own `Cache-Control` (a `reply` with
|
|
788
|
+
`cache-control: public, max-age=…`, or a middleware after `secureHeaders`)
|
|
789
|
+
keeps it: the option only fills a header the response lacks.
|
|
790
|
+
|
|
791
|
+
**Fix:** drop the route's header, or give it the same value. Before, the
|
|
792
|
+
route's own header wins:
|
|
793
|
+
|
|
794
|
+
```ts
|
|
795
|
+
app.get('/the-page', ({ reply }) =>
|
|
796
|
+
reply(200, html, { headers: { 'cache-control': 'public, max-age=60' } }),
|
|
797
|
+
);
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
After, the route leaves it to `secureHeaders` (or says `private, no-store`
|
|
801
|
+
itself):
|
|
802
|
+
|
|
803
|
+
```ts
|
|
804
|
+
app.get('/the-page', ({ reply }) => reply(200, html));
|
|
805
|
+
|
|
806
|
+
const response = await app.request('/the-page');
|
|
807
|
+
console.log(response.headers.get('cache-control')); // private, no-store
|
|
808
|
+
```
|
|
809
|
+
|
|
504
810
|
### `X-Powered-By` or `Server` is still sent
|
|
505
811
|
|
|
506
812
|
**When:** `hidePoweredBy` is on (the default), and one of them is on the
|
|
@@ -635,3 +941,19 @@ const app = alxia()
|
|
|
635
941
|
.use(secureHeaders())
|
|
636
942
|
.get('/early', ({ reply }) => reply(200, 'now covered'));
|
|
637
943
|
```
|
|
944
|
+
|
|
945
|
+
### `Clear-Site-Data` is missing on a client-side sign-out
|
|
946
|
+
|
|
947
|
+
**When:** a form sign-out empties the cache, but a client-side one (the button, a navigation) does not. `Clear-Site-Data` is on the response of `POST /sign-out` and absent from `POST /sign-out.data`.
|
|
948
|
+
|
|
949
|
+
**Why:** React Router's single fetch posts the route's `.data` URL, a path `use('/sign-out', …)` does not cover. A route cannot set the header there either: that answer carries a redirect's cookies, not its other headers.
|
|
950
|
+
|
|
951
|
+
**Fix:** mount the middleware on both paths:
|
|
952
|
+
|
|
953
|
+
```ts
|
|
954
|
+
const cleared = clearSiteData(['cache']);
|
|
955
|
+
|
|
956
|
+
const app = alxia()
|
|
957
|
+
.use('/sign-out', cleared)
|
|
958
|
+
.use('/sign-out.data', cleared);
|
|
959
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alxia/secure-headers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Secure HTTP headers for alxia: HSTS, CSP, nosniff and the rest, each one adjustable",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
]
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@alxia/core": "^0.
|
|
44
|
+
"@alxia/core": "^0.15.1",
|
|
45
45
|
"@types/bun": "^1.4.2",
|
|
46
46
|
"zod": "^4.2.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@alxia/core": "^0.
|
|
49
|
+
"@alxia/core": "^0.15.1",
|
|
50
50
|
"typescript": "^6.0.3 || ^7.0.0"
|
|
51
51
|
}
|
|
52
52
|
}
|