Skip to content

Commit 52dd8eb

Browse files
authored
Content-Security-Policy: better error when value should be quoted
It's easy to forget to quote directive value entries like `'self'` and `'none'`. Someone [recently ran into this][0] and was confused by the error message. This makes the error message clearer by telling you that something needs to be quoted. ```diff -...invalid directive value for "img-src" +...invalid directive value for "img-src". "self" should be quoted ``` [0]: #482
1 parent 4af4777 commit 52dd8eb

File tree

3 files changed

+67
-47
lines changed

3 files changed

+67
-47
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Changed
6+
7+
- `Content-Security-Policy` gives a better error when a directive value, like `self`, should be quoted. See [#482](https://github.com/helmetjs/helmet/issues/482)
8+
39
## 8.0.0
410

511
### Changed

middlewares/content-security-policy/index.ts

+39-31
Original file line numberDiff line numberDiff line change
@@ -71,22 +71,37 @@ const getDefaultDirectives = (): Record<
7171
const dashify = (str: string): string =>
7272
str.replace(/[A-Z]/g, (capitalLetter) => "-" + capitalLetter.toLowerCase());
7373

74-
const isDirectiveValueInvalid = (directiveValue: string): boolean =>
75-
/;|,/.test(directiveValue);
76-
77-
const isDirectiveValueEntryInvalid = (directiveValueEntry: string): boolean =>
78-
SHOULD_BE_QUOTED.has(directiveValueEntry) ||
79-
directiveValueEntry.startsWith("nonce-") ||
80-
directiveValueEntry.startsWith("sha256-") ||
81-
directiveValueEntry.startsWith("sha384-") ||
82-
directiveValueEntry.startsWith("sha512-");
83-
84-
const invalidDirectiveValueError = (directiveName: string): Error =>
85-
new Error(
86-
`Content-Security-Policy received an invalid directive value for ${JSON.stringify(
87-
directiveName,
88-
)}`,
89-
);
74+
const assertDirectiveValueIsValid = (
75+
directiveName: string,
76+
directiveValue: string,
77+
): void => {
78+
if (/;|,/.test(directiveValue)) {
79+
throw new Error(
80+
`Content-Security-Policy received an invalid directive value for ${JSON.stringify(
81+
directiveName,
82+
)}`,
83+
);
84+
}
85+
};
86+
87+
const assertDirectiveValueEntryIsValid = (
88+
directiveName: string,
89+
directiveValueEntry: string,
90+
): void => {
91+
if (
92+
SHOULD_BE_QUOTED.has(directiveValueEntry) ||
93+
directiveValueEntry.startsWith("nonce-") ||
94+
directiveValueEntry.startsWith("sha256-") ||
95+
directiveValueEntry.startsWith("sha384-") ||
96+
directiveValueEntry.startsWith("sha512-")
97+
) {
98+
throw new Error(
99+
`Content-Security-Policy received an invalid directive value for ${JSON.stringify(
100+
directiveName,
101+
)}. ${JSON.stringify(directiveValueEntry)} should be quoted`,
102+
);
103+
}
104+
};
90105

91106
function normalizeDirectives(
92107
options: Readonly<ContentSecurityPolicyOptions>,
@@ -161,13 +176,9 @@ function normalizeDirectives(
161176
}
162177

163178
for (const element of directiveValue) {
164-
if (
165-
typeof element === "string" &&
166-
(isDirectiveValueInvalid(element) ||
167-
isDirectiveValueEntryInvalid(element))
168-
) {
169-
throw invalidDirectiveValueError(directiveName);
170-
}
179+
if (typeof element !== "string") continue;
180+
assertDirectiveValueIsValid(directiveName, element);
181+
assertDirectiveValueEntryIsValid(directiveName, element);
171182
}
172183

173184
result.set(directiveName, directiveValue);
@@ -215,21 +226,18 @@ function getHeaderValue(
215226
for (const element of rawDirectiveValue) {
216227
if (typeof element === "function") {
217228
const newElement = element(req, res);
218-
if (isDirectiveValueEntryInvalid(newElement)) {
219-
return invalidDirectiveValueError(directiveName);
220-
}
229+
assertDirectiveValueEntryIsValid(directiveName, newElement);
221230
directiveValue += " " + newElement;
222231
} else {
223232
directiveValue += " " + element;
224233
}
225234
}
226235

227-
if (!directiveValue) {
228-
result.push(directiveName);
229-
} else if (isDirectiveValueInvalid(directiveValue)) {
230-
return invalidDirectiveValueError(directiveName);
231-
} else {
236+
if (directiveValue) {
237+
assertDirectiveValueIsValid(directiveName, directiveValue);
232238
result.push(`${directiveName}${directiveValue}`);
239+
} else {
240+
result.push(directiveName);
233241
}
234242
}
235243

test/content-security-policy.test.ts

+22-16
Original file line numberDiff line numberDiff line change
@@ -373,13 +373,7 @@ describe("Content-Security-Policy middleware", () => {
373373
});
374374

375375
it("throws if any directive values are invalid", () => {
376-
const invalidValues = [
377-
";",
378-
",",
379-
"hello;world",
380-
"hello,world",
381-
...shouldBeQuoted,
382-
];
376+
const invalidValues = [";", ",", "hello;world", "hello,world"];
383377
for (const invalidValue of invalidValues) {
384378
assert.throws(
385379
() => {
@@ -397,6 +391,23 @@ describe("Content-Security-Policy middleware", () => {
397391
},
398392
);
399393
}
394+
395+
for (const invalidDirectiveEntry of shouldBeQuoted) {
396+
assert.throws(
397+
() => {
398+
contentSecurityPolicy({
399+
useDefaults: false,
400+
directives: {
401+
"default-src": "'self'",
402+
"something-else": [invalidDirectiveEntry],
403+
},
404+
});
405+
},
406+
{
407+
message: `Content-Security-Policy received an invalid directive value for "something-else". "${invalidDirectiveEntry}" should be quoted`,
408+
},
409+
);
410+
}
400411
});
401412

402413
it("errors if any directive values are invalid when a function returns", async () => {
@@ -421,19 +432,14 @@ describe("Content-Security-Policy middleware", () => {
421432
// eslint-disable-next-line @typescript-eslint/no-unused-vars
422433
_next: () => void,
423434
) => {
424-
res.statusCode = 500;
425-
res.setHeader("Content-Type", "application/json");
426-
res.end(
427-
JSON.stringify({ helmetTestError: true, message: err.message }),
435+
const isOk = err.message.startsWith(
436+
'Content-Security-Policy received an invalid directive value for "default-src"',
428437
);
438+
res.end(JSON.stringify(isOk));
429439
},
430440
);
431441

432-
await supertest(app).get("/").expect(500, {
433-
helmetTestError: true,
434-
message:
435-
'Content-Security-Policy received an invalid directive value for "default-src"',
436-
});
442+
await supertest(app).get("/").expect(200, "true");
437443
}),
438444
);
439445
});

0 commit comments

Comments
 (0)