Skip to content

Commit aeba96b

Browse files
committed
Content-Security-Policy: throw if directive value lacks necessary quotes
Closes [#454]. [#454]: #454
1 parent e2eb103 commit aeba96b

File tree

3 files changed

+65
-96
lines changed

3 files changed

+65
-96
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## 8.0.0
44

5+
### Changed
6+
7+
- **Breaking:** `Content-Security-Policy` middleware now throws an error if a directive should have quotes but does not, such as `self` instead of `'self'`. See [#454](https://github.com/helmetjs/helmet/issues/454)
8+
59
### Removed
610

711
- **Breaking:** Drop support for Node 16 and 17. Node 18+ is now required

Diff for: middlewares/content-security-policy/index.ts

+20-29
Original file line numberDiff line numberDiff line change
@@ -76,22 +76,19 @@ const dashify = (str: string): string =>
7676
const isDirectiveValueInvalid = (directiveValue: string): boolean =>
7777
/;|,/.test(directiveValue);
7878

79-
const shouldDirectiveValueEntryBeQuoted = (
80-
directiveValueEntry: string,
81-
): boolean =>
79+
const isDirectiveValueEntryInvalid = (directiveValueEntry: string): boolean =>
8280
SHOULD_BE_QUOTED.has(directiveValueEntry) ||
8381
directiveValueEntry.startsWith("nonce-") ||
8482
directiveValueEntry.startsWith("sha256-") ||
8583
directiveValueEntry.startsWith("sha384-") ||
8684
directiveValueEntry.startsWith("sha512-");
8785

88-
const warnIfDirectiveValueEntryShouldBeQuoted = (value: string): void => {
89-
if (shouldDirectiveValueEntryBeQuoted(value)) {
90-
console.warn(
91-
`Content-Security-Policy got directive value \`${value}\` which should be single-quoted and changed to \`'${value}'\`. This will be an error in future versions of Helmet.`,
92-
);
93-
}
94-
};
86+
const invalidDirectiveValueError = (directiveName: string): Error =>
87+
new Error(
88+
`Content-Security-Policy received an invalid directive value for ${JSON.stringify(
89+
directiveName,
90+
)}`,
91+
);
9592

9693
function normalizeDirectives(
9794
options: Readonly<ContentSecurityPolicyOptions>,
@@ -166,15 +163,12 @@ function normalizeDirectives(
166163
}
167164

168165
for (const element of directiveValue) {
169-
if (typeof element === "string") {
170-
if (isDirectiveValueInvalid(element)) {
171-
throw new Error(
172-
`Content-Security-Policy received an invalid directive value for ${JSON.stringify(
173-
directiveName,
174-
)}`,
175-
);
176-
}
177-
warnIfDirectiveValueEntryShouldBeQuoted(element);
166+
if (
167+
typeof element === "string" &&
168+
(isDirectiveValueInvalid(element) ||
169+
isDirectiveValueEntryInvalid(element))
170+
) {
171+
throw invalidDirectiveValueError(directiveName);
178172
}
179173
}
180174

@@ -216,15 +210,16 @@ function getHeaderValue(
216210
res: ServerResponse,
217211
normalizedDirectives: Readonly<NormalizedDirectives>,
218212
): string | Error {
219-
let err: undefined | Error;
220213
const result: string[] = [];
221214

222-
normalizedDirectives.forEach((rawDirectiveValue, directiveName) => {
215+
for (const [directiveName, rawDirectiveValue] of normalizedDirectives) {
223216
let directiveValue = "";
224217
for (const element of rawDirectiveValue) {
225218
if (typeof element === "function") {
226219
const newElement = element(req, res);
227-
warnIfDirectiveValueEntryShouldBeQuoted(newElement);
220+
if (isDirectiveValueEntryInvalid(newElement)) {
221+
return invalidDirectiveValueError(directiveName);
222+
}
228223
directiveValue += " " + newElement;
229224
} else {
230225
directiveValue += " " + element;
@@ -234,17 +229,13 @@ function getHeaderValue(
234229
if (!directiveValue) {
235230
result.push(directiveName);
236231
} else if (isDirectiveValueInvalid(directiveValue)) {
237-
err = new Error(
238-
`Content-Security-Policy received an invalid directive value for ${JSON.stringify(
239-
directiveName,
240-
)}`,
241-
);
232+
return invalidDirectiveValueError(directiveName);
242233
} else {
243234
result.push(`${directiveName}${directiveValue}`);
244235
}
245-
});
236+
}
246237

247-
return err ? err : result.join(";");
238+
return result.join(";");
248239
}
249240

250241
const contentSecurityPolicy: ContentSecurityPolicy =

Diff for: test/content-security-policy.test.ts

+41-67
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,13 @@ describe("Content-Security-Policy middleware", () => {
348348
});
349349

350350
it("throws if any directive values are invalid", () => {
351-
const invalidValues = [";", ",", "hello;world", "hello,world"];
351+
const invalidValues = [
352+
";",
353+
",",
354+
"hello;world",
355+
"hello,world",
356+
...shouldBeQuoted,
357+
];
352358
for (const invalidValue of invalidValues) {
353359
expect(() => {
354360
contentSecurityPolicy({
@@ -364,75 +370,43 @@ describe("Content-Security-Policy middleware", () => {
364370
}
365371
});
366372

367-
it("at call time, warns if directive values lack quotes when they should", () => {
368-
jest.spyOn(console, "warn").mockImplementation(() => {});
369-
370-
contentSecurityPolicy({
371-
directives: { defaultSrc: shouldBeQuoted },
372-
});
373-
374-
expect(console.warn).toHaveBeenCalledTimes(shouldBeQuoted.length);
375-
for (const directiveValue of shouldBeQuoted) {
376-
expect(console.warn).toHaveBeenCalledWith(
377-
`Content-Security-Policy got directive value \`${directiveValue}\` which should be single-quoted and changed to \`'${directiveValue}'\`. This will be an error in future versions of Helmet.`,
378-
);
379-
}
380-
});
381-
382373
it("errors if any directive values are invalid when a function returns", async () => {
383-
const app = connect()
384-
.use(
385-
contentSecurityPolicy({
386-
useDefaults: false,
387-
directives: {
388-
defaultSrc: ["'self'", () => "bad;value"],
389-
},
390-
}),
391-
)
392-
.use(
393-
(
394-
err: Error,
395-
_req: IncomingMessage,
396-
res: ServerResponse,
397-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
398-
_next: () => void,
399-
) => {
400-
res.statusCode = 500;
401-
res.setHeader("Content-Type", "application/json");
402-
res.end(
403-
JSON.stringify({ helmetTestError: true, message: err.message }),
374+
const badDirectiveValueEntries = ["bad;value", ...shouldBeQuoted];
375+
376+
await Promise.all(
377+
badDirectiveValueEntries.map(async (directiveValueEntry) => {
378+
const app = connect()
379+
.use(
380+
contentSecurityPolicy({
381+
useDefaults: false,
382+
directives: {
383+
defaultSrc: ["'self'", () => directiveValueEntry],
384+
},
385+
}),
386+
)
387+
.use(
388+
(
389+
err: Error,
390+
_req: IncomingMessage,
391+
res: ServerResponse,
392+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
393+
_next: () => void,
394+
) => {
395+
res.statusCode = 500;
396+
res.setHeader("Content-Type", "application/json");
397+
res.end(
398+
JSON.stringify({ helmetTestError: true, message: err.message }),
399+
);
400+
},
404401
);
405-
},
406-
);
407-
408-
await supertest(app).get("/").expect(500, {
409-
helmetTestError: true,
410-
message:
411-
'Content-Security-Policy received an invalid directive value for "default-src"',
412-
});
413-
});
414-
415-
it("at request time, warns if directive values lack quotes when they should", async () => {
416-
jest.spyOn(console, "warn").mockImplementation(() => {});
417-
418-
const app = connect()
419-
.use(
420-
contentSecurityPolicy({
421-
directives: { defaultSrc: shouldBeQuoted },
422-
}),
423-
)
424-
.use((_req: IncomingMessage, res: ServerResponse) => {
425-
res.end();
426-
});
427402

428-
await supertest(app).get("/").expect(200);
429-
430-
expect(console.warn).toHaveBeenCalledTimes(shouldBeQuoted.length);
431-
for (const directiveValue of shouldBeQuoted) {
432-
expect(console.warn).toHaveBeenCalledWith(
433-
`Content-Security-Policy got directive value \`${directiveValue}\` which should be single-quoted and changed to \`'${directiveValue}'\`. This will be an error in future versions of Helmet.`,
434-
);
435-
}
403+
await supertest(app).get("/").expect(500, {
404+
helmetTestError: true,
405+
message:
406+
'Content-Security-Policy received an invalid directive value for "default-src"',
407+
});
408+
}),
409+
);
436410
});
437411

438412
it("throws if default-src is missing", () => {

0 commit comments

Comments
 (0)