-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
127 lines (113 loc) · 3.75 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
enum CspKeywords {
"'none'",
"'report-sample'",
"'self'",
"'strict-dynamic'",
"'unsafe-allow-redirects'",
"'unsafe-eval'",
"'unsafe-hashes'",
"'unsafe-inline'",
"data:",
}
enum CspDirectiveKeys {
"base-uri",
"block-all-mixed-content",
"connect-src",
"default-src",
"font-src",
"form-action",
"frame-ancestors",
"frame-src",
"img-src",
"manifest-src",
"media-src",
"navigate-to",
"object-src",
"plugin-types",
"prefetch-src",
"report-to",
"report-uri",
"require-sri-for",
"require-trusted-types-for",
"sandbox",
"script-src-attr",
"script-src-elem",
"script-src",
"style-src-attr",
"style-src-elem",
"style-src",
"trusted-types",
"upgrade-insecure-requests",
"worker-src",
}
enum KeyOnlyDirectives {
"block-all-mixed-content",
"upgrade-insecure-requests",
}
/**
* TypeScript's Object.values(enum) creates an array
* with values _and_ keys (which are numbers),
* hence the typecasting later in the code (as string[])
*/
const keyOnlyDirectives = Object.values(KeyOnlyDirectives);
const cspDirectiveNames = Object.values(CspDirectiveKeys);
type CspDirectiveKey = keyof typeof CspDirectiveKeys;
type CspDirectivePredefinedValue = keyof typeof CspKeywords;
export type CspSource = {
[k in CspDirectiveKey]?: CspDirectivePredefinedValue[] | string[];
};
const supportedHashingAlgorithms = ["sha256", "sha384", "sha512"];
const validateSource = (input: unknown): { valid: true; source: CspSource } | { valid: false; errors: string[] } => {
// Condition: input must be an object
if (typeof input !== "object") {
return { valid: false, errors: ["Input must be an object"] };
}
const errors = Object.entries(input as object)
.map((directive) => {
const [directiveKey, directiveValuesArray] = directive;
// Condition: key must be a known directive
if (!cspDirectiveNames.includes(directiveKey)) {
return `Unknown directive '${directiveKey}'`;
}
// Condition: When a key is a known key-only-directive, it's value must be an empty array
if (keyOnlyDirectives.includes(directiveKey) && directiveValuesArray.length > 0) {
return `Key-only directive '${directiveKey}' must have an empty array as value`;
}
// Condition: value of known (non key-only) CSP directive must contain only CSP keywords, domain-like strings (more then 3 letters, with at least one dot), or strings that begin with the name of a supported hashing algorithm and not contain any dot
if (
(!keyOnlyDirectives.includes(directiveKey) && directiveValuesArray.length === 0) ||
!directiveValuesArray.every((value: string) => {
return (
Object.values(CspKeywords).includes(value) || //
(value.includes(".") && value.length > 3) ||
(supportedHashingAlgorithms.includes(value.substring(1, 7)) && !value.includes("."))
);
})
) {
return `Invalid value for '${directiveKey}'`;
}
})
.filter((error) => error);
if (errors.length) {
return { valid: false, errors: errors as string[] };
}
return { valid: true, source: input as CspSource };
};
const reduceCspObjectToString = (source: CspSource) => (csp: string, directiveKey: string) => {
const key = directiveKey as CspDirectiveKey;
if (keyOnlyDirectives.includes(key)) {
csp += `${key}; `;
} else {
csp += `${key} ${(source[key] as string[]).join(" ")}; `;
}
return csp;
};
export const generate = (input: CspSource): string => {
const validationResult = validateSource(input);
if (!validationResult.valid) {
throw new Error(validationResult.errors.join(", "));
}
const { source } = validationResult;
return Object.keys(source).reduce(reduceCspObjectToString(source), "").trim();
};
export default generate;