@baukit/csp 0.19.2 → 0.20.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 +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/policy.d.ts +12 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +39 -2
- package/dist/policy.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/policy.ts +55 -2
- package/src/vendor-sources.test.ts +98 -0
package/README.md
CHANGED
|
@@ -15,6 +15,10 @@ const policy = contentSecurityPolicy({
|
|
|
15
15
|
|
|
16
16
|
`contentSecurityPolicy` throws on any script source that is not a `'sha256-...'` hash, and leaves
|
|
17
17
|
out `upgrade-insecure-requests` only when an origin is plain `http:`.
|
|
18
|
+
`vendorScripts` adds origins to `script-src`, `vendorFrames` adds them to `frame-src`, and
|
|
19
|
+
`blobFrames: true` adds `blob:` to `frame-src`. `assertProductionOrigins(origins)` throws for `http:`
|
|
20
|
+
origins, embedded credentials and loopback hosts, so a production export fails when a local `.env`
|
|
21
|
+
leaks into the build.
|
|
18
22
|
`developmentContentSecurityPolicy` adds inline styles and the HMR socket for the dev server.
|
|
19
23
|
`unhashedInlineScripts` lists inline scripts a built page carries that the policy does not hash.
|
|
20
24
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { SECURITY_HEADERS } from './headers.js';
|
|
2
|
-
export { clerkFrontendApiOrigin, contentSecurityPolicy, developmentContentSecurityPolicy, hashSource, inlineScripts, originOf, unhashedInlineScripts, type PolicyOptions, type PolicyOrigins, type StyleSources, } from './policy.js';
|
|
2
|
+
export { assertProductionOrigins, clerkFrontendApiOrigin, contentSecurityPolicy, developmentContentSecurityPolicy, hashSource, inlineScripts, originOf, unhashedInlineScripts, type PolicyOptions, type PolicyOrigins, type StyleSources, } from './policy.js';
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,gCAAgC,EAChC,UAAU,EACV,aAAa,EACb,QAAQ,EACR,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,gCAAgC,EAChC,UAAU,EACV,aAAa,EACb,QAAQ,EACR,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { SECURITY_HEADERS } from './headers.js';
|
|
2
|
-
export { clerkFrontendApiOrigin, contentSecurityPolicy, developmentContentSecurityPolicy, hashSource, inlineScripts, originOf, unhashedInlineScripts, } from './policy.js';
|
|
2
|
+
export { assertProductionOrigins, clerkFrontendApiOrigin, contentSecurityPolicy, developmentContentSecurityPolicy, hashSource, inlineScripts, originOf, unhashedInlineScripts, } from './policy.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,gCAAgC,EAChC,UAAU,EACV,aAAa,EACb,QAAQ,EACR,qBAAqB,GAItB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,gCAAgC,EAChC,UAAU,EACV,aAAa,EACb,QAAQ,EACR,qBAAqB,GAItB,MAAM,aAAa,CAAC"}
|
package/dist/policy.d.ts
CHANGED
|
@@ -12,12 +12,18 @@ export interface PolicyOrigins {
|
|
|
12
12
|
readonly suiteHub?: string | undefined;
|
|
13
13
|
readonly vendorConnect?: readonly string[];
|
|
14
14
|
readonly vendorImages?: readonly string[];
|
|
15
|
+
/** Origins added to `script-src`, for vendor scripts such as the Clerk frontend API. */
|
|
16
|
+
readonly vendorScripts?: readonly string[];
|
|
17
|
+
/** Origins added to `frame-src` next to the issuer. */
|
|
18
|
+
readonly vendorFrames?: readonly string[];
|
|
15
19
|
}
|
|
16
20
|
export interface PolicyOptions {
|
|
17
21
|
readonly styles: StyleSources;
|
|
18
22
|
readonly origins: PolicyOrigins;
|
|
19
23
|
/** `'sha256-...'` sources for every inline `<script>` the build emits. */
|
|
20
24
|
readonly scriptHashes: readonly string[];
|
|
25
|
+
/** Adds `blob:` to `frame-src`, for in-app previews that render a blob URL in an iframe. */
|
|
26
|
+
readonly blobFrames?: boolean;
|
|
21
27
|
}
|
|
22
28
|
/** Returns the origin of a configured URL, or `undefined` when it is unset. */
|
|
23
29
|
export declare function originOf(value: string | undefined): string | undefined;
|
|
@@ -39,4 +45,10 @@ export declare function developmentContentSecurityPolicy(options: PolicyOptions,
|
|
|
39
45
|
export declare function unhashedInlineScripts(html: string, policy: string, hash: (script: string) => string): string[];
|
|
40
46
|
/** Decodes Clerk's frontend API host from a publishable key (`pk_<env>_<base64 host$>`). */
|
|
41
47
|
export declare function clerkFrontendApiOrigin(publishableKey: string | undefined): string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Throws when a configured origin is not a plain HTTPS origin on a public host.
|
|
50
|
+
* A production export calls it so a leaked local `.env` cannot ship `http:` or
|
|
51
|
+
* loopback origins in the policy.
|
|
52
|
+
*/
|
|
53
|
+
export declare function assertProductionOrigins(origins: PolicyOrigins): void;
|
|
42
54
|
//# sourceMappingURL=policy.d.ts.map
|
package/dist/policy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mGAAmG;AACnG,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,kBAAkB,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mGAAmG;AACnG,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,kBAAkB,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,wFAAwF;IACxF,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,uDAAuD;IACvD,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,4FAA4F;IAC5F,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAQD,+EAA+E;AAC/E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMtE;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEpD;AAED,uDAAuD;AACvD,wBAAgB,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAMvD;AAqED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAOpE;AAED;;;GAGG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,aAAa,EACtB,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,MAAM,CAMR;AAED,sFAAsF;AACtF,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAC/B,MAAM,EAAE,CASV;AAED,4FAA4F;AAC5F,wBAAgB,sBAAsB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAO7F;AAiBD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAapE"}
|
package/dist/policy.js
CHANGED
|
@@ -39,17 +39,24 @@ function originList(origins) {
|
|
|
39
39
|
origins.suiteHub,
|
|
40
40
|
...(origins.vendorConnect ?? []),
|
|
41
41
|
...(origins.vendorImages ?? []),
|
|
42
|
+
...(origins.vendorScripts ?? []),
|
|
43
|
+
...(origins.vendorFrames ?? []),
|
|
42
44
|
];
|
|
43
45
|
}
|
|
44
46
|
function usesPlainHttp(origins) {
|
|
45
47
|
return originList(origins).some((origin) => origin?.startsWith('http:') === true);
|
|
46
48
|
}
|
|
49
|
+
function frameSources(options) {
|
|
50
|
+
const { origins } = options;
|
|
51
|
+
const list = sources(origins.issuer, ...(origins.vendorFrames ?? []), options.blobFrames === true ? 'blob:' : undefined);
|
|
52
|
+
return list.length === 0 ? [NONE] : list;
|
|
53
|
+
}
|
|
47
54
|
function directives(options) {
|
|
48
55
|
const { origins } = options;
|
|
49
56
|
const issuer = origins.issuer;
|
|
50
57
|
return [
|
|
51
58
|
['default-src', [SELF]],
|
|
52
|
-
['script-src', sources(SELF, ...options.scriptHashes)],
|
|
59
|
+
['script-src', sources(SELF, ...(origins.vendorScripts ?? []), ...options.scriptHashes)],
|
|
53
60
|
['style-src', options.styles === 'files' ? [SELF] : [SELF, INLINE]],
|
|
54
61
|
['img-src', sources(SELF, 'data:', 'blob:', origins.media, ...(origins.vendorImages ?? []))],
|
|
55
62
|
['media-src', sources(SELF, 'blob:', origins.media)],
|
|
@@ -57,7 +64,7 @@ function directives(options) {
|
|
|
57
64
|
'connect-src',
|
|
58
65
|
sources(SELF, origins.api, issuer, origins.analytics, origins.suiteHub, ...(origins.vendorConnect ?? [])),
|
|
59
66
|
],
|
|
60
|
-
['frame-src',
|
|
67
|
+
['frame-src', frameSources(options)],
|
|
61
68
|
['worker-src', [SELF]],
|
|
62
69
|
['manifest-src', [SELF]],
|
|
63
70
|
['object-src', [NONE]],
|
|
@@ -110,4 +117,34 @@ export function clerkFrontendApiOrigin(publishableKey) {
|
|
|
110
117
|
const host = atob(encoded).replace(/\$$/u, '');
|
|
111
118
|
return originOf(`https://${host}`);
|
|
112
119
|
}
|
|
120
|
+
const IPV4_MAPPED_LOOPBACK = /^\[::ffff:7f[0-9a-f]{2}:/u;
|
|
121
|
+
function isLoopbackHost(hostname) {
|
|
122
|
+
const host = hostname.toLowerCase().replace(/\.$/u, '');
|
|
123
|
+
return (host === 'localhost' ||
|
|
124
|
+
host.endsWith('.localhost') ||
|
|
125
|
+
host === '[::1]' ||
|
|
126
|
+
host === '[::]' ||
|
|
127
|
+
host === '0.0.0.0' ||
|
|
128
|
+
host.startsWith('127.') ||
|
|
129
|
+
IPV4_MAPPED_LOOPBACK.test(host));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Throws when a configured origin is not a plain HTTPS origin on a public host.
|
|
133
|
+
* A production export calls it so a leaked local `.env` cannot ship `http:` or
|
|
134
|
+
* loopback origins in the policy.
|
|
135
|
+
*/
|
|
136
|
+
export function assertProductionOrigins(origins) {
|
|
137
|
+
for (const origin of originList(origins)) {
|
|
138
|
+
if (origin === undefined) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const url = new URL(origin);
|
|
142
|
+
if (url.protocol !== 'https:' || url.username !== '' || url.password !== '') {
|
|
143
|
+
throw new Error(`not an HTTPS origin without credentials: ${origin}`);
|
|
144
|
+
}
|
|
145
|
+
if (isLoopbackHost(url.hostname)) {
|
|
146
|
+
throw new Error(`loopback origin in a production policy: ${origin}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
113
150
|
//# sourceMappingURL=policy.js.map
|
package/dist/policy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/policy.ts
CHANGED
|
@@ -14,6 +14,10 @@ export interface PolicyOrigins {
|
|
|
14
14
|
readonly suiteHub?: string | undefined;
|
|
15
15
|
readonly vendorConnect?: readonly string[];
|
|
16
16
|
readonly vendorImages?: readonly string[];
|
|
17
|
+
/** Origins added to `script-src`, for vendor scripts such as the Clerk frontend API. */
|
|
18
|
+
readonly vendorScripts?: readonly string[];
|
|
19
|
+
/** Origins added to `frame-src` next to the issuer. */
|
|
20
|
+
readonly vendorFrames?: readonly string[];
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export interface PolicyOptions {
|
|
@@ -21,6 +25,8 @@ export interface PolicyOptions {
|
|
|
21
25
|
readonly origins: PolicyOrigins;
|
|
22
26
|
/** `'sha256-...'` sources for every inline `<script>` the build emits. */
|
|
23
27
|
readonly scriptHashes: readonly string[];
|
|
28
|
+
/** Adds `blob:` to `frame-src`, for in-app previews that render a blob URL in an iframe. */
|
|
29
|
+
readonly blobFrames?: boolean;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
const SELF = "'self'";
|
|
@@ -65,6 +71,8 @@ function originList(origins: PolicyOrigins): (string | undefined)[] {
|
|
|
65
71
|
origins.suiteHub,
|
|
66
72
|
...(origins.vendorConnect ?? []),
|
|
67
73
|
...(origins.vendorImages ?? []),
|
|
74
|
+
...(origins.vendorScripts ?? []),
|
|
75
|
+
...(origins.vendorFrames ?? []),
|
|
68
76
|
];
|
|
69
77
|
}
|
|
70
78
|
|
|
@@ -72,12 +80,22 @@ function usesPlainHttp(origins: PolicyOrigins): boolean {
|
|
|
72
80
|
return originList(origins).some((origin) => origin?.startsWith('http:') === true);
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
function frameSources(options: PolicyOptions): string[] {
|
|
84
|
+
const { origins } = options;
|
|
85
|
+
const list = sources(
|
|
86
|
+
origins.issuer,
|
|
87
|
+
...(origins.vendorFrames ?? []),
|
|
88
|
+
options.blobFrames === true ? 'blob:' : undefined,
|
|
89
|
+
);
|
|
90
|
+
return list.length === 0 ? [NONE] : list;
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
function directives(options: PolicyOptions): [string, string[]][] {
|
|
76
94
|
const { origins } = options;
|
|
77
95
|
const issuer = origins.issuer;
|
|
78
96
|
return [
|
|
79
97
|
['default-src', [SELF]],
|
|
80
|
-
['script-src', sources(SELF, ...options.scriptHashes)],
|
|
98
|
+
['script-src', sources(SELF, ...(origins.vendorScripts ?? []), ...options.scriptHashes)],
|
|
81
99
|
['style-src', options.styles === 'files' ? [SELF] : [SELF, INLINE]],
|
|
82
100
|
['img-src', sources(SELF, 'data:', 'blob:', origins.media, ...(origins.vendorImages ?? []))],
|
|
83
101
|
['media-src', sources(SELF, 'blob:', origins.media)],
|
|
@@ -92,7 +110,7 @@ function directives(options: PolicyOptions): [string, string[]][] {
|
|
|
92
110
|
...(origins.vendorConnect ?? []),
|
|
93
111
|
),
|
|
94
112
|
],
|
|
95
|
-
['frame-src',
|
|
113
|
+
['frame-src', frameSources(options)],
|
|
96
114
|
['worker-src', [SELF]],
|
|
97
115
|
['manifest-src', [SELF]],
|
|
98
116
|
['object-src', [NONE]],
|
|
@@ -160,3 +178,38 @@ export function clerkFrontendApiOrigin(publishableKey: string | undefined): stri
|
|
|
160
178
|
const host = atob(encoded).replace(/\$$/u, '');
|
|
161
179
|
return originOf(`https://${host}`);
|
|
162
180
|
}
|
|
181
|
+
|
|
182
|
+
const IPV4_MAPPED_LOOPBACK = /^\[::ffff:7f[0-9a-f]{2}:/u;
|
|
183
|
+
|
|
184
|
+
function isLoopbackHost(hostname: string): boolean {
|
|
185
|
+
const host = hostname.toLowerCase().replace(/\.$/u, '');
|
|
186
|
+
return (
|
|
187
|
+
host === 'localhost' ||
|
|
188
|
+
host.endsWith('.localhost') ||
|
|
189
|
+
host === '[::1]' ||
|
|
190
|
+
host === '[::]' ||
|
|
191
|
+
host === '0.0.0.0' ||
|
|
192
|
+
host.startsWith('127.') ||
|
|
193
|
+
IPV4_MAPPED_LOOPBACK.test(host)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Throws when a configured origin is not a plain HTTPS origin on a public host.
|
|
199
|
+
* A production export calls it so a leaked local `.env` cannot ship `http:` or
|
|
200
|
+
* loopback origins in the policy.
|
|
201
|
+
*/
|
|
202
|
+
export function assertProductionOrigins(origins: PolicyOrigins): void {
|
|
203
|
+
for (const origin of originList(origins)) {
|
|
204
|
+
if (origin === undefined) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const url = new URL(origin);
|
|
208
|
+
if (url.protocol !== 'https:' || url.username !== '' || url.password !== '') {
|
|
209
|
+
throw new Error(`not an HTTPS origin without credentials: ${origin}`);
|
|
210
|
+
}
|
|
211
|
+
if (isLoopbackHost(url.hostname)) {
|
|
212
|
+
throw new Error(`loopback origin in a production policy: ${origin}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { assertProductionOrigins, contentSecurityPolicy } from './index.js';
|
|
4
|
+
|
|
5
|
+
function directive(policy: string, name: string): string[] | undefined {
|
|
6
|
+
return policy
|
|
7
|
+
.split(';')
|
|
8
|
+
.map((part) => part.trim().split(' '))
|
|
9
|
+
.find(([directiveName]) => directiveName === name)
|
|
10
|
+
?.slice(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const base = { styles: 'files', scriptHashes: [] } as const;
|
|
14
|
+
|
|
15
|
+
describe('vendor script and frame sources', () => {
|
|
16
|
+
it('adds vendor script origins to script-src before the hashes', () => {
|
|
17
|
+
const policy = contentSecurityPolicy({
|
|
18
|
+
...base,
|
|
19
|
+
scriptHashes: ["'sha256-Ym9vdA=='"],
|
|
20
|
+
origins: { vendorScripts: ['https://clerk.example.test'] },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(directive(policy, 'script-src')).toEqual([
|
|
24
|
+
"'self'",
|
|
25
|
+
'https://clerk.example.test',
|
|
26
|
+
"'sha256-Ym9vdA=='",
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('adds vendor frames and blob: to frame-src next to the issuer', () => {
|
|
31
|
+
const policy = contentSecurityPolicy({
|
|
32
|
+
...base,
|
|
33
|
+
blobFrames: true,
|
|
34
|
+
origins: { issuer: 'https://login.example.test', vendorFrames: ['https://pay.example.test'] },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(directive(policy, 'frame-src')).toEqual([
|
|
38
|
+
'https://login.example.test',
|
|
39
|
+
'https://pay.example.test',
|
|
40
|
+
'blob:',
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('keeps frame-src at none without an issuer, vendor frame or blob frames', () => {
|
|
45
|
+
expect(directive(contentSecurityPolicy({ ...base, origins: {} }), 'frame-src')).toEqual([
|
|
46
|
+
"'none'",
|
|
47
|
+
]);
|
|
48
|
+
expect(
|
|
49
|
+
directive(contentSecurityPolicy({ ...base, blobFrames: true, origins: {} }), 'frame-src'),
|
|
50
|
+
).toEqual(['blob:']);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('treats a plain http vendor script origin as a local build', () => {
|
|
54
|
+
const policy = contentSecurityPolicy({
|
|
55
|
+
...base,
|
|
56
|
+
origins: { vendorScripts: ['http://localhost:9000'] },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(policy).not.toContain('upgrade-insecure-requests');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('production origin guard', () => {
|
|
64
|
+
it('accepts public HTTPS origins', () => {
|
|
65
|
+
expect(() => {
|
|
66
|
+
assertProductionOrigins({
|
|
67
|
+
api: 'https://api.example.test',
|
|
68
|
+
vendorScripts: ['https://clerk.example.test'],
|
|
69
|
+
});
|
|
70
|
+
}).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it.each([
|
|
74
|
+
'http://api.example.test',
|
|
75
|
+
'https://user:secret@api.example.test',
|
|
76
|
+
'https://localhost:8080',
|
|
77
|
+
'https://app.localhost',
|
|
78
|
+
'https://127.0.0.1',
|
|
79
|
+
'https://127.1.2.3:9',
|
|
80
|
+
'https://0.0.0.0',
|
|
81
|
+
'https://[::1]',
|
|
82
|
+
'https://[::ffff:7f00:1]',
|
|
83
|
+
'https://localhost.',
|
|
84
|
+
])('rejects %s', (origin) => {
|
|
85
|
+
expect(() => {
|
|
86
|
+
assertProductionOrigins({ api: origin });
|
|
87
|
+
}).toThrow();
|
|
88
|
+
expect(() => {
|
|
89
|
+
assertProductionOrigins({ vendorFrames: [origin] });
|
|
90
|
+
}).toThrow();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('skips unset origins', () => {
|
|
94
|
+
expect(() => {
|
|
95
|
+
assertProductionOrigins({ api: undefined, issuer: undefined });
|
|
96
|
+
}).not.toThrow();
|
|
97
|
+
});
|
|
98
|
+
});
|