csprefabricate 0.2.3 → 0.4.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 +116 -10
- package/dist/baseline.d.ts +8 -0
- package/dist/baseline.js +245 -0
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.js +66 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +2 -0
- package/dist/types.d.ts +1 -1
- package/dist/utils.d.ts +10 -3
- package/dist/utils.js +32 -11
- package/package.json +2 -3
- package/dist/test/helpers.test.d.ts +0 -1
- package/dist/test/helpers.test.js +0 -23
- package/dist/test/utils.test.d.ts +0 -1
- package/dist/test/utils.test.js +0 -69
package/README.md
CHANGED
|
@@ -14,20 +14,126 @@ Currently `csprefabricate`:
|
|
|
14
14
|
|
|
15
15
|
- Validates directive names
|
|
16
16
|
- Supports providing a list of TLDs for a given domain name
|
|
17
|
+
- Provides warnings for insecure or incomplete CSP configurations, with options to disable specific warnings
|
|
18
|
+
|
|
19
|
+
## Common CSP Issues
|
|
20
|
+
|
|
21
|
+
By default, `csprefabricate` will warn you about common CSP issues, such as:
|
|
22
|
+
|
|
23
|
+
- Overly permissive sources (e.g. using `*`)
|
|
24
|
+
- Missing recommended directives (i.e. `object-src`, `base-uri`, `form-action`)
|
|
25
|
+
- Use of `'unsafe-inline'` in `script-src`, even if nonces or hashes are present
|
|
26
|
+
- Missing nonces or hashes when using `'unsafe-inline'` in `script-src`
|
|
27
|
+
- Allowing `data:` in `img-src` or `media-src`
|
|
28
|
+
|
|
29
|
+
You can control which warnings are shown by passing an optional `WarningOptions` object to the `create` function:
|
|
17
30
|
|
|
18
31
|
```typescript
|
|
19
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
create,
|
|
34
|
+
Directive,
|
|
35
|
+
ContentSecurityPolicy,
|
|
36
|
+
WarningOptions,
|
|
37
|
+
} from "csprefabricate";
|
|
38
|
+
|
|
39
|
+
const csp: ContentSecurityPolicy = {
|
|
40
|
+
[Directive.SCRIPT_SRC]: ["*"],
|
|
41
|
+
[Directive.IMG_SRC]: ["data:"],
|
|
42
|
+
};
|
|
20
43
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
44
|
+
// Disable all warnings
|
|
45
|
+
const warningOptions: WarningOptions = {
|
|
46
|
+
overlyPermissive: false,
|
|
47
|
+
missingDirectives: false,
|
|
48
|
+
unsafeInline: false,
|
|
49
|
+
missingNonceOrHash: false,
|
|
50
|
+
dataUri: false,
|
|
51
|
+
};
|
|
25
52
|
|
|
26
|
-
|
|
27
|
-
// > "default-src 'self'; img-src 'self' *.google.com *.google.com.au;",
|
|
53
|
+
create(csp, warningOptions);
|
|
28
54
|
```
|
|
29
55
|
|
|
30
|
-
|
|
56
|
+
You can selectively enable or disable specific warnings as needed.
|
|
57
|
+
|
|
58
|
+
## Real World Examples
|
|
59
|
+
|
|
60
|
+
### Example 1: Basic Strict Policy
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import {create, Directive, ContentSecurityPolicy} from "csprefabricate";
|
|
31
64
|
|
|
32
|
-
|
|
33
|
-
|
|
65
|
+
const csp: ContentSecurityPolicy = {
|
|
66
|
+
[Directive.DEFAULT_SRC]: ["'self'"],
|
|
67
|
+
[Directive.SCRIPT_SRC]: ["'self'"],
|
|
68
|
+
[Directive.STYLE_SRC]: ["'self'"],
|
|
69
|
+
[Directive.IMG_SRC]: ["'self'"],
|
|
70
|
+
[Directive.OBJECT_SRC]: ["'none'"],
|
|
71
|
+
[Directive.BASE_URI]: ["'self'"],
|
|
72
|
+
[Directive.FORM_ACTION]: ["'self'"],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const cspString = create(csp);
|
|
76
|
+
// "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self';"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Example 2: Allowing Google Analytics
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import {create, Directive, ContentSecurityPolicy} from "csprefabricate";
|
|
83
|
+
|
|
84
|
+
const csp: ContentSecurityPolicy = {
|
|
85
|
+
[Directive.DEFAULT_SRC]: ["'self'"],
|
|
86
|
+
[Directive.SCRIPT_SRC]: ["'self'", "*.googletagmanager.com"],
|
|
87
|
+
[Directive.STYLE_SRC]: ["'self'"],
|
|
88
|
+
[Directive.IMG_SRC]: [
|
|
89
|
+
"'self'",
|
|
90
|
+
"https://*.google-analytics.com",
|
|
91
|
+
"https://*.googletagmanager.com",
|
|
92
|
+
],
|
|
93
|
+
[Directive.OBJECT_SRC]: ["'none'"],
|
|
94
|
+
[Directive.BASE_URI]: ["'self'"],
|
|
95
|
+
[Directive.FORM_ACTION]: ["'self'"],
|
|
96
|
+
[Directive.CONNECT_SRC]: [
|
|
97
|
+
"'self'",
|
|
98
|
+
"https://*.google-analytics.com",
|
|
99
|
+
"https://*.analytics.google.com",
|
|
100
|
+
"https://*.googletagmanager.com",
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const cspString = create(csp);
|
|
105
|
+
// "default-src 'self'; script-src 'self' *.googletagmanager.com; style-src 'self'; img-src 'self' https://*.google-analytics.com https://*.googletagmanager.com; object-src 'none'; base-uri 'self'; form-action 'self'; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com;"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Example 3: Using TLD Expansion for Multiple Domains
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import {create, Directive, ContentSecurityPolicy} from "csprefabricate";
|
|
112
|
+
|
|
113
|
+
const csp: ContentSecurityPolicy = {
|
|
114
|
+
[Directive.IMG_SRC]: ["self", {"*.example": [".com", ".co.uk", ".net"]}],
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const cspString = create(csp);
|
|
118
|
+
// "img-src 'self' *.example.com *.example.co.uk *.example.net;"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Baseline Recommended CSPs
|
|
122
|
+
|
|
123
|
+
You can quickly generate a recommended Content Security Policy for common use cases using built-in baselines.
|
|
124
|
+
|
|
125
|
+
Available Baselines:
|
|
126
|
+
|
|
127
|
+
- BASELINE_STRICT_CSP
|
|
128
|
+
- GOOGLE_ANALYTICS_CSP
|
|
129
|
+
- GOOGLE_ANALYTICS_WITH_SIGNALS_CSP
|
|
130
|
+
|
|
131
|
+
### Google Analytics Baseline CSP
|
|
132
|
+
|
|
133
|
+
Allow Google Analytics and Tag Manager:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import {create, Baseline} from "csprefabricate";
|
|
137
|
+
|
|
138
|
+
const cspString = create(Baseline.GOOGLE_ANALYTICS_CSP);
|
|
139
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ContentSecurityPolicy } from "./types";
|
|
2
|
+
export declare const BASELINE_STRICT_CSP: ContentSecurityPolicy;
|
|
3
|
+
/**
|
|
4
|
+
* Google Analytics Content Security Policy based on the official guidelines.
|
|
5
|
+
* https://developers.google.com/tag-platform/security/guides/csp#google_analytics_4_google_analytics
|
|
6
|
+
*/
|
|
7
|
+
export declare const GOOGLE_ANALYTICS_CSP: ContentSecurityPolicy;
|
|
8
|
+
export declare const GOOGLE_ANALYTICS_WITH_SIGNALS_CSP: ContentSecurityPolicy;
|
package/dist/baseline.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Directive } from "./types";
|
|
2
|
+
// List of supported domains for Google Signals from https://www.google.com/supported_domains
|
|
3
|
+
const googleSupportedTLDs = [
|
|
4
|
+
".com",
|
|
5
|
+
".ad",
|
|
6
|
+
".ae",
|
|
7
|
+
".com.af",
|
|
8
|
+
".com.ag",
|
|
9
|
+
".al",
|
|
10
|
+
".am",
|
|
11
|
+
".co.ao",
|
|
12
|
+
".com.ar",
|
|
13
|
+
".as",
|
|
14
|
+
".at",
|
|
15
|
+
".com.au",
|
|
16
|
+
".az",
|
|
17
|
+
".ba",
|
|
18
|
+
".com.bd",
|
|
19
|
+
".be",
|
|
20
|
+
".bf",
|
|
21
|
+
".bg",
|
|
22
|
+
".com.bh",
|
|
23
|
+
".bi",
|
|
24
|
+
".bj",
|
|
25
|
+
".com.bn",
|
|
26
|
+
".com.bo",
|
|
27
|
+
".com.br",
|
|
28
|
+
".bs",
|
|
29
|
+
".bt",
|
|
30
|
+
".co.bw",
|
|
31
|
+
".by",
|
|
32
|
+
".com.bz",
|
|
33
|
+
".ca",
|
|
34
|
+
".cd",
|
|
35
|
+
".cf",
|
|
36
|
+
".cg",
|
|
37
|
+
".ch",
|
|
38
|
+
".ci",
|
|
39
|
+
".co.ck",
|
|
40
|
+
".cl",
|
|
41
|
+
".cm",
|
|
42
|
+
".cn",
|
|
43
|
+
".com.co",
|
|
44
|
+
".co.cr",
|
|
45
|
+
".com.cu",
|
|
46
|
+
".cv",
|
|
47
|
+
".com.cy",
|
|
48
|
+
".cz",
|
|
49
|
+
".de",
|
|
50
|
+
".dj",
|
|
51
|
+
".dk",
|
|
52
|
+
".dm",
|
|
53
|
+
".com.do",
|
|
54
|
+
".dz",
|
|
55
|
+
".com.ec",
|
|
56
|
+
".ee",
|
|
57
|
+
".com.eg",
|
|
58
|
+
".es",
|
|
59
|
+
".com.et",
|
|
60
|
+
".fi",
|
|
61
|
+
".com.fj",
|
|
62
|
+
".fm",
|
|
63
|
+
".fr",
|
|
64
|
+
".ga",
|
|
65
|
+
".ge",
|
|
66
|
+
".gg",
|
|
67
|
+
".com.gh",
|
|
68
|
+
".com.gi",
|
|
69
|
+
".gl",
|
|
70
|
+
".gm",
|
|
71
|
+
".gr",
|
|
72
|
+
".com.gt",
|
|
73
|
+
".gy",
|
|
74
|
+
".com.hk",
|
|
75
|
+
".hn",
|
|
76
|
+
".hr",
|
|
77
|
+
".ht",
|
|
78
|
+
".hu",
|
|
79
|
+
".co.id",
|
|
80
|
+
".ie",
|
|
81
|
+
".co.il",
|
|
82
|
+
".im",
|
|
83
|
+
".co.in",
|
|
84
|
+
".iq",
|
|
85
|
+
".is",
|
|
86
|
+
".it",
|
|
87
|
+
".je",
|
|
88
|
+
".com.jm",
|
|
89
|
+
".jo",
|
|
90
|
+
".co.jp",
|
|
91
|
+
".co.ke",
|
|
92
|
+
".com.kh",
|
|
93
|
+
".ki",
|
|
94
|
+
".kg",
|
|
95
|
+
".co.kr",
|
|
96
|
+
".com.kw",
|
|
97
|
+
".kz",
|
|
98
|
+
".la",
|
|
99
|
+
".com.lb",
|
|
100
|
+
".li",
|
|
101
|
+
".lk",
|
|
102
|
+
".co.ls",
|
|
103
|
+
".lt",
|
|
104
|
+
".lu",
|
|
105
|
+
".lv",
|
|
106
|
+
".com.ly",
|
|
107
|
+
".co.ma",
|
|
108
|
+
".md",
|
|
109
|
+
".me",
|
|
110
|
+
".mg",
|
|
111
|
+
".mk",
|
|
112
|
+
".ml",
|
|
113
|
+
".com.mm",
|
|
114
|
+
".mn",
|
|
115
|
+
".com.mt",
|
|
116
|
+
".mu",
|
|
117
|
+
".mv",
|
|
118
|
+
".mw",
|
|
119
|
+
".com.mx",
|
|
120
|
+
".com.my",
|
|
121
|
+
".co.mz",
|
|
122
|
+
".com.na",
|
|
123
|
+
".com.ng",
|
|
124
|
+
".com.ni",
|
|
125
|
+
".ne",
|
|
126
|
+
".nl",
|
|
127
|
+
".no",
|
|
128
|
+
".com.np",
|
|
129
|
+
".nr",
|
|
130
|
+
".nu",
|
|
131
|
+
".co.nz",
|
|
132
|
+
".com.om",
|
|
133
|
+
".com.pa",
|
|
134
|
+
".com.pe",
|
|
135
|
+
".com.pg",
|
|
136
|
+
".com.ph",
|
|
137
|
+
".com.pk",
|
|
138
|
+
".pl",
|
|
139
|
+
".pn",
|
|
140
|
+
".com.pr",
|
|
141
|
+
".ps",
|
|
142
|
+
".pt",
|
|
143
|
+
".com.py",
|
|
144
|
+
".com.qa",
|
|
145
|
+
".ro",
|
|
146
|
+
".ru",
|
|
147
|
+
".rw",
|
|
148
|
+
".com.sa",
|
|
149
|
+
".com.sb",
|
|
150
|
+
".sc",
|
|
151
|
+
".se",
|
|
152
|
+
".com.sg",
|
|
153
|
+
".sh",
|
|
154
|
+
".si",
|
|
155
|
+
".sk",
|
|
156
|
+
".com.sl",
|
|
157
|
+
".sn",
|
|
158
|
+
".so",
|
|
159
|
+
".sm",
|
|
160
|
+
".sr",
|
|
161
|
+
".st",
|
|
162
|
+
".com.sv",
|
|
163
|
+
".td",
|
|
164
|
+
".tg",
|
|
165
|
+
".co.th",
|
|
166
|
+
".com.tj",
|
|
167
|
+
".tl",
|
|
168
|
+
".tm",
|
|
169
|
+
".tn",
|
|
170
|
+
".to",
|
|
171
|
+
".com.tr",
|
|
172
|
+
".tt",
|
|
173
|
+
".com.tw",
|
|
174
|
+
".co.tz",
|
|
175
|
+
".com.ua",
|
|
176
|
+
".co.ug",
|
|
177
|
+
".co.uk",
|
|
178
|
+
".com.uy",
|
|
179
|
+
".co.uz",
|
|
180
|
+
".com.vc",
|
|
181
|
+
".co.ve",
|
|
182
|
+
".co.vi",
|
|
183
|
+
".com.vn",
|
|
184
|
+
".vu",
|
|
185
|
+
".ws",
|
|
186
|
+
".rs",
|
|
187
|
+
".co.za",
|
|
188
|
+
".co.zm",
|
|
189
|
+
".co.zw",
|
|
190
|
+
".cat",
|
|
191
|
+
];
|
|
192
|
+
export const BASELINE_STRICT_CSP = {
|
|
193
|
+
[Directive.DEFAULT_SRC]: ["'self'"],
|
|
194
|
+
[Directive.SCRIPT_SRC]: ["'self'"],
|
|
195
|
+
[Directive.STYLE_SRC]: ["'self'"],
|
|
196
|
+
[Directive.IMG_SRC]: ["'self'"],
|
|
197
|
+
[Directive.OBJECT_SRC]: ["'none'"],
|
|
198
|
+
[Directive.BASE_URI]: ["'self'"],
|
|
199
|
+
[Directive.FORM_ACTION]: ["'self'"],
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Google Analytics Content Security Policy based on the official guidelines.
|
|
203
|
+
* https://developers.google.com/tag-platform/security/guides/csp#google_analytics_4_google_analytics
|
|
204
|
+
*/
|
|
205
|
+
export const GOOGLE_ANALYTICS_CSP = {
|
|
206
|
+
...BASELINE_STRICT_CSP,
|
|
207
|
+
[Directive.DEFAULT_SRC]: ["'self'"],
|
|
208
|
+
[Directive.SCRIPT_SRC]: ["'self'", "*.googletagmanager.com"],
|
|
209
|
+
[Directive.IMG_SRC]: [
|
|
210
|
+
"'self'",
|
|
211
|
+
"https://*.google-analytics.com",
|
|
212
|
+
"https://*.googletagmanager.com",
|
|
213
|
+
],
|
|
214
|
+
[Directive.CONNECT_SRC]: [
|
|
215
|
+
"'self'",
|
|
216
|
+
"https://*.google-analytics.com",
|
|
217
|
+
"https://*.analytics.google.com",
|
|
218
|
+
"https://*.googletagmanager.com",
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
export const GOOGLE_ANALYTICS_WITH_SIGNALS_CSP = {
|
|
222
|
+
...BASELINE_STRICT_CSP,
|
|
223
|
+
...GOOGLE_ANALYTICS_CSP,
|
|
224
|
+
[Directive.IMG_SRC]: [
|
|
225
|
+
"'self'",
|
|
226
|
+
"https://*.google-analytics.com",
|
|
227
|
+
"https://*.googletagmanager.com",
|
|
228
|
+
"https://*.g.doubleclick.net",
|
|
229
|
+
"https://*.google.com",
|
|
230
|
+
{ "https://*.google.": googleSupportedTLDs },
|
|
231
|
+
],
|
|
232
|
+
[Directive.CONNECT_SRC]: [
|
|
233
|
+
"'self'",
|
|
234
|
+
"https://*.google-analytics.com",
|
|
235
|
+
"https://*.googletagmanager.com",
|
|
236
|
+
"https://*.g.doubleclick.net",
|
|
237
|
+
"https://pagead2.googlesyndication.com",
|
|
238
|
+
{ "https://*.google": googleSupportedTLDs },
|
|
239
|
+
],
|
|
240
|
+
[Directive.FRAME_SRC]: [
|
|
241
|
+
"'self'",
|
|
242
|
+
"https://td.doubleclick.net",
|
|
243
|
+
"https://www.googletagmanager.com",
|
|
244
|
+
],
|
|
245
|
+
};
|
package/dist/helpers.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
import { ContentSecurityPolicy } from "./types";
|
|
2
|
+
export interface WarningOptions {
|
|
3
|
+
overlyPermissive?: boolean;
|
|
4
|
+
missingDirectives?: boolean;
|
|
5
|
+
unsafeInline?: boolean;
|
|
6
|
+
missingNonceOrHash?: boolean;
|
|
7
|
+
dataUri?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function warnOnCspIssues(csp: ContentSecurityPolicy, overrides?: WarningOptions): void;
|
|
1
10
|
export declare const isValidDirective: (directive: string) => boolean;
|
|
2
11
|
export declare const formatRule: (rule: string) => string;
|
package/dist/helpers.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { Directive } from "./types";
|
|
2
|
+
const DEFAULT_WARNINGS = {
|
|
3
|
+
overlyPermissive: true,
|
|
4
|
+
missingDirectives: true,
|
|
5
|
+
unsafeInline: true,
|
|
6
|
+
missingNonceOrHash: true,
|
|
7
|
+
dataUri: true,
|
|
8
|
+
};
|
|
1
9
|
const validDirectives = [
|
|
2
10
|
"default-src",
|
|
3
11
|
"script-src",
|
|
@@ -33,5 +41,63 @@ const specialRules = [
|
|
|
33
41
|
"strict-dynamic",
|
|
34
42
|
"unsafe-hashes",
|
|
35
43
|
];
|
|
44
|
+
export function warnOnCspIssues(csp, overrides = {}) {
|
|
45
|
+
const options = { ...DEFAULT_WARNINGS, ...overrides };
|
|
46
|
+
// 1. Overly permissive: * in script-src, style-src, etc.
|
|
47
|
+
if (options.overlyPermissive) {
|
|
48
|
+
[
|
|
49
|
+
Directive.SCRIPT_SRC,
|
|
50
|
+
Directive.STYLE_SRC,
|
|
51
|
+
Directive.IMG_SRC,
|
|
52
|
+
Directive.CONNECT_SRC,
|
|
53
|
+
].forEach((directive) => {
|
|
54
|
+
const rules = csp[directive];
|
|
55
|
+
if (Array.isArray(rules) && rules.includes("*")) {
|
|
56
|
+
console.warn(`[CSPrefabricate] Overly permissive: '*' found in ${directive}`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// 2. Missing important directives
|
|
61
|
+
if (options.missingDirectives) {
|
|
62
|
+
[
|
|
63
|
+
Directive.OBJECT_SRC,
|
|
64
|
+
Directive.BASE_URI,
|
|
65
|
+
Directive.FORM_ACTION,
|
|
66
|
+
].forEach((directive) => {
|
|
67
|
+
if (!(directive in csp)) {
|
|
68
|
+
console.warn(`[CSPrefabricate] Missing recommended directive: ${directive}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// 3. Unsafe inline
|
|
73
|
+
if (options.unsafeInline) {
|
|
74
|
+
[Directive.SCRIPT_SRC, Directive.STYLE_SRC].forEach((directive) => {
|
|
75
|
+
const rules = csp[directive];
|
|
76
|
+
if (Array.isArray(rules) && rules.includes("'unsafe-inline'")) {
|
|
77
|
+
console.warn(`[CSPrefabricate] 'unsafe-inline' found in ${directive}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// 4. Missing nonce or hash in script-src if 'unsafe-inline' is present
|
|
82
|
+
if (options.missingNonceOrHash) {
|
|
83
|
+
const rules = csp[Directive.SCRIPT_SRC];
|
|
84
|
+
if (Array.isArray(rules) && rules.includes("'unsafe-inline'")) {
|
|
85
|
+
const hasNonceOrHash = rules.some((r) => typeof r === "string" &&
|
|
86
|
+
(r.startsWith("'nonce-") || r.startsWith("'sha")));
|
|
87
|
+
if (!hasNonceOrHash) {
|
|
88
|
+
console.warn(`[CSPrefabricate] 'unsafe-inline' in script-src without nonce or hash`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// 5. Permitting data: in img-src or media-src
|
|
93
|
+
if (options.dataUri) {
|
|
94
|
+
[Directive.IMG_SRC, Directive.MEDIA_SRC].forEach((directive) => {
|
|
95
|
+
const rules = csp[directive];
|
|
96
|
+
if (Array.isArray(rules) && rules.includes("data:")) {
|
|
97
|
+
console.warn(`[CSPrefabricate] 'data:' allowed in ${directive}`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
36
102
|
export const isValidDirective = (directive) => validDirectives.includes(directive);
|
|
37
103
|
export const formatRule = (rule) => specialRules.includes(rule) ? `'${rule}'` : rule;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Directive } from "./types";
|
|
2
2
|
import { create } from "./utils";
|
|
3
|
-
|
|
3
|
+
import * as Baseline from "./baseline";
|
|
4
|
+
export { Baseline };
|
|
5
|
+
export { create, Directive };
|
|
6
|
+
export type { ContentSecurityPolicy } from "./types";
|
package/dist/index.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -55,4 +55,4 @@ interface ContentSecurityPolicy {
|
|
|
55
55
|
[Directive.UPGRADE_INSECURE_REQUESTS]?: BlankDirectiveRule;
|
|
56
56
|
[Directive.BLOCK_ALL_MIXED_CONTENT]?: BlankDirectiveRule;
|
|
57
57
|
}
|
|
58
|
-
export { ContentSecurityPolicy, Rules, Directive };
|
|
58
|
+
export { ContentSecurityPolicy, Rules, Directive, BasicDirectiveRule };
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export declare const
|
|
1
|
+
import { WarningOptions } from "./helpers";
|
|
2
|
+
import { ContentSecurityPolicy, BasicDirectiveRule } from "./types";
|
|
3
|
+
export declare const processRules: (rules: BasicDirectiveRule) => string;
|
|
4
|
+
/**
|
|
5
|
+
* Creates a CSP string from a ContentSecurityPolicy object.
|
|
6
|
+
* Filters out invalid directives and formats the CSP string.
|
|
7
|
+
* @param obj - The ContentSecurityPolicy object.
|
|
8
|
+
* @returns The formatted CSP string.
|
|
9
|
+
*/
|
|
10
|
+
export declare const create: (obj: ContentSecurityPolicy, warningOptions?: WarningOptions) => string;
|
package/dist/utils.js
CHANGED
|
@@ -1,26 +1,47 @@
|
|
|
1
|
-
import { formatRule, isValidDirective } from "./helpers";
|
|
1
|
+
import { formatRule, isValidDirective, warnOnCspIssues, } from "./helpers";
|
|
2
2
|
export const processRules = (rules) => {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// Flatten and deduplicate rules
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
for (const rule of rules) {
|
|
5
6
|
if (typeof rule === "object") {
|
|
6
|
-
|
|
7
|
+
for (const [domain, tlds] of Object.entries(rule)) {
|
|
8
|
+
for (const tld of tlds) {
|
|
9
|
+
seen.add(`${domain}${tld}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
7
12
|
}
|
|
8
13
|
else {
|
|
9
|
-
|
|
14
|
+
seen.add(formatRule(rule));
|
|
10
15
|
}
|
|
11
|
-
}
|
|
12
|
-
|
|
16
|
+
}
|
|
17
|
+
return Array.from(seen).join(" ");
|
|
13
18
|
};
|
|
14
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Creates a CSP string from a ContentSecurityPolicy object.
|
|
21
|
+
* Filters out invalid directives and formats the CSP string.
|
|
22
|
+
* @param obj - The ContentSecurityPolicy object.
|
|
23
|
+
* @returns The formatted CSP string.
|
|
24
|
+
*/
|
|
25
|
+
export const create = (obj, warningOptions) => {
|
|
26
|
+
warnOnCspIssues(obj, warningOptions);
|
|
15
27
|
const entries = Object.entries(obj);
|
|
16
28
|
const cspString = entries
|
|
17
29
|
.filter(([directive, _rules]) => {
|
|
18
30
|
const isValid = isValidDirective(directive);
|
|
19
31
|
if (!isValid) {
|
|
20
|
-
console.warn(`"${directive}" is not a valid CSP directive and has been ignored.`);
|
|
32
|
+
console.warn(`[CSPrefabricate] "${directive}" is not a valid CSP directive and has been ignored.`);
|
|
21
33
|
}
|
|
22
34
|
return isValid;
|
|
23
35
|
})
|
|
24
|
-
.map(([directive, rules]) =>
|
|
25
|
-
|
|
36
|
+
.map(([directive, rules]) => {
|
|
37
|
+
if (Array.isArray(rules)) {
|
|
38
|
+
// Filter out non-string/object values at runtime
|
|
39
|
+
const filtered = rules.filter((r) => typeof r === "string" ||
|
|
40
|
+
(typeof r === "object" && r !== null));
|
|
41
|
+
const processed = processRules(filtered);
|
|
42
|
+
return processed ? `${directive} ${processed}` : `${directive}`;
|
|
43
|
+
}
|
|
44
|
+
return `${directive}`;
|
|
45
|
+
});
|
|
46
|
+
return cspString.length > 0 ? `${cspString.join("; ")};` : "";
|
|
26
47
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "csprefabricate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"packageManager": "yarn@4.5.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"devDependencies": {
|
|
@@ -20,11 +20,10 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "tsc --project tsconfig.build.json",
|
|
23
|
+
"functional-test": "yarn build && tsx --test src/test/functional/functional.test.js",
|
|
23
24
|
"pack": "npm pack",
|
|
24
25
|
"prepack": "yarn typecheck && yarn test && yarn build",
|
|
25
|
-
"prettier": "prettier . --write",
|
|
26
26
|
"prepublish": "yarn version check",
|
|
27
|
-
"publish": "npm publish",
|
|
28
27
|
"test": "tsx --test src/test/**/*test.ts",
|
|
29
28
|
"typecheck": "tsc --noEmit",
|
|
30
29
|
"lint": "eslint ."
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { formatRule, isValidDirective } from "../helpers";
|
|
4
|
-
import { Directive } from "../types";
|
|
5
|
-
describe("Helpers tests", () => {
|
|
6
|
-
describe("isValidDirective", () => {
|
|
7
|
-
it("Returns true if directive is valid", () => {
|
|
8
|
-
assert.strictEqual(isValidDirective(Directive.BASE_URI), true);
|
|
9
|
-
assert.strictEqual(isValidDirective("default-src"), true);
|
|
10
|
-
});
|
|
11
|
-
it("Returns false if directive is invalid", () => {
|
|
12
|
-
assert.strictEqual(isValidDirective("some-src"), false);
|
|
13
|
-
});
|
|
14
|
-
});
|
|
15
|
-
describe("formatRule", () => {
|
|
16
|
-
it("Formats special rules with single quotes", () => {
|
|
17
|
-
assert.strictEqual(formatRule("self"), `'self'`);
|
|
18
|
-
});
|
|
19
|
-
it("Returns non-special rules", () => {
|
|
20
|
-
assert.strictEqual(formatRule("google.com"), `google.com`);
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/test/utils.test.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { create, processRules } from "../utils";
|
|
4
|
-
import { Directive } from "../types";
|
|
5
|
-
describe("Utils tests", () => {
|
|
6
|
-
describe("processRules", () => {
|
|
7
|
-
it("Processes rules provided as an array of strings (simple)", () => {
|
|
8
|
-
const rules = ["self", "*.google.com", "*.google.com.au"];
|
|
9
|
-
assert.strictEqual(processRules(rules), `'self' *.google.com *.google.com.au`);
|
|
10
|
-
});
|
|
11
|
-
it("Processes rules provided a complex list of tlds", () => {
|
|
12
|
-
const rules = ["self", { "*.google": [".com", ".com.au"] }];
|
|
13
|
-
assert.strictEqual(processRules(rules), `'self' *.google.com *.google.com.au`);
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
describe("create", () => {
|
|
17
|
-
it("Formats a CSP string with all rules", () => {
|
|
18
|
-
const csp = {
|
|
19
|
-
[Directive.DEFAULT_SRC]: ["self"],
|
|
20
|
-
[Directive.SCRIPT_SRC]: ["self", "js.example.com"],
|
|
21
|
-
[Directive.STYLE_SRC]: ["self", "css.example.com"],
|
|
22
|
-
[Directive.IMG_SRC]: [
|
|
23
|
-
"self",
|
|
24
|
-
{ "*.google": [".com", ".com.au"] },
|
|
25
|
-
],
|
|
26
|
-
[Directive.CONNECT_SRC]: ["self"],
|
|
27
|
-
[Directive.FONT_SRC]: ["self", "font.example.com"],
|
|
28
|
-
[Directive.OBJECT_SRC]: ["none"],
|
|
29
|
-
[Directive.MEDIA_SRC]: ["self", "media.example.com"],
|
|
30
|
-
[Directive.FRAME_SRC]: ["self"],
|
|
31
|
-
[Directive.SANDBOX]: ["allow-scripts"],
|
|
32
|
-
[Directive.REPORT_URI]: ["/my-report-uri"],
|
|
33
|
-
[Directive.CHILD_SRC]: ["self"],
|
|
34
|
-
[Directive.FORM_ACTION]: ["self"],
|
|
35
|
-
[Directive.FRAME_ANCESTORS]: ["none"],
|
|
36
|
-
[Directive.PLUGIN_TYPES]: ["application/pdf"],
|
|
37
|
-
[Directive.BASE_URI]: ["self"],
|
|
38
|
-
[Directive.REPORT_TO]: ["myGroupName"],
|
|
39
|
-
[Directive.WORKER_SRC]: ["none"],
|
|
40
|
-
[Directive.MANIFEST_SRC]: ["none"],
|
|
41
|
-
[Directive.PREFETCH_SRC]: ["none"],
|
|
42
|
-
[Directive.NAVIGATE_TO]: ["example.com"],
|
|
43
|
-
[Directive.REQUIRE_TRUSTED_TYPES_FOR]: ["script"],
|
|
44
|
-
[Directive.TRUSTED_TYPES]: ["none"],
|
|
45
|
-
[Directive.UPGRADE_INSECURE_REQUESTS]: null,
|
|
46
|
-
[Directive.BLOCK_ALL_MIXED_CONTENT]: null,
|
|
47
|
-
};
|
|
48
|
-
const cspString = create(csp);
|
|
49
|
-
assert.strictEqual(cspString, "default-src 'self'; script-src 'self' js.example.com; style-src 'self' css.example.com; img-src 'self' *.google.com *.google.com.au; connect-src 'self'; font-src 'self' font.example.com; object-src 'none'; media-src 'self' media.example.com; frame-src 'self'; sandbox allow-scripts; report-uri /my-report-uri; child-src 'self'; form-action 'self'; frame-ancestors 'none'; plugin-types application/pdf; base-uri 'self'; report-to myGroupName; worker-src 'none'; manifest-src 'none'; prefetch-src 'none'; navigate-to example.com; require-trusted-types-for script; trusted-types 'none'; upgrade-insecure-requests; block-all-mixed-content;");
|
|
50
|
-
});
|
|
51
|
-
it("Handles blank directives", () => {
|
|
52
|
-
const csp = {
|
|
53
|
-
[Directive.SANDBOX]: [],
|
|
54
|
-
};
|
|
55
|
-
const cspString = create(csp);
|
|
56
|
-
assert.strictEqual(cspString, "sandbox;");
|
|
57
|
-
});
|
|
58
|
-
it("Ignores invalid directives", () => {
|
|
59
|
-
const csp = {
|
|
60
|
-
[Directive.DEFAULT_SRC]: ["self"],
|
|
61
|
-
// @ts-expect-error deliberate testing of invalid directive
|
|
62
|
-
["invalid-directive"]: ["self"],
|
|
63
|
-
[Directive.IMG_SRC]: ["my.domain.com"]
|
|
64
|
-
};
|
|
65
|
-
const cspString = create(csp);
|
|
66
|
-
assert.strictEqual(cspString, "default-src 'self'; img-src my.domain.com;");
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
});
|