Skip to content

Commit c172ce8

Browse files
authored
fix(csp): allow duplicate report-* directives (#151)
* Added duplicate report-* directives detection * addressed some nits
1 parent 3566b6c commit c172ce8

File tree

6 files changed

+64
-15
lines changed

6 files changed

+64
-15
lines changed

scan

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/analyzer/cspParser.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const DIRECTIVES_DISALLOWED_IN_META = [
77
"report-uri",
88
"sandbox",
99
];
10+
const ALLOWED_DUPLICATE_KEYS = new Set(["report-uri", "report-to"]);
11+
export const DUPLICATE_WARNINGS_KEY = "_observatory_duplicate_key_warnings";
1012

1113
/**
1214
* Parse CSP from meta tags, weeding out directives
@@ -24,6 +26,10 @@ export function parseCspMeta(cspList) {
2426
}
2527

2628
/**
29+
* The returned Map has the directive as the key and a Set of sources as the value.
30+
* If there are allowed duplicates detected, the first one is kept and the rest are discarded,
31+
* and an entry in the final Map is added with the key "_observatory_duplicate_key_warnings"
32+
* and the directive's name as the value.
2733
*
2834
* @param {string[]} cspList
2935
* @returns {Map<string, Set<string>>}
@@ -44,6 +50,8 @@ export function parseCsp(cspList) {
4450

4551
/** @type {Map<string, {source: string, index: number, keep: boolean}[]>} */
4652
const csp = new Map();
53+
/** @type {Set<string>} */
54+
const duplicate_warnings = new Set();
4755

4856
for (const [policyIndex, policy] of cleanCspList.entries()) {
4957
const directiveSeenBeforeThisPolicy = new Set();
@@ -59,11 +67,16 @@ export function parseCsp(cspList) {
5967
const directive = directiveEntry.toLowerCase();
6068

6169
// While technically valid in that you just use the first entry, we are saying that repeated
62-
// directives are invalid so that people notice it
70+
// directives are invalid so that people notice it. The exception are duplicate report-uri
71+
// and report-to directives, which we allow.
6372
if (directiveSeenBeforeThisPolicy.has(directive)) {
64-
throw new Error(
65-
`Duplicate directive ${directive} in policy ${policyIndex}`
66-
);
73+
if (ALLOWED_DUPLICATE_KEYS.has(directive)) {
74+
duplicate_warnings.add(directive);
75+
} else {
76+
throw new Error(
77+
`Duplicate directive ${directive} in policy ${policyIndex}`
78+
);
79+
}
6780
} else {
6881
directiveSeenBeforeThisPolicy.add(directive);
6982
}
@@ -146,7 +159,9 @@ export function parseCsp(cspList) {
146159
: new Set(["'none'"]),
147160
])
148161
);
149-
162+
if (duplicate_warnings.size) {
163+
finalCsp.set(DUPLICATE_WARNINGS_KEY, duplicate_warnings);
164+
}
150165
return finalCsp;
151166
}
152167

src/analyzer/tests/csp.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import {
44
} from "../../headers.js";
55
import { Requests, Policy, BaseOutput } from "../../types.js";
66
import { Expectation } from "../../types.js";
7-
import { parseCsp, parseCspMeta } from "../cspParser.js";
7+
import {
8+
DUPLICATE_WARNINGS_KEY,
9+
parseCsp,
10+
parseCspMeta,
11+
} from "../cspParser.js";
812
import { getHttpHeaders } from "../utils.js";
913

1014
const DANGEROUSLY_BROAD = new Set([
@@ -47,6 +51,7 @@ export class CspOutput extends BaseOutput {
4751
Expectation.CspImplementedWithUnsafeEval,
4852
Expectation.CspImplementedWithUnsafeInline,
4953
Expectation.CspImplementedWithInsecureScheme,
54+
Expectation.CspImplementedButDuplicateDirectives,
5055
Expectation.CspHeaderInvalid,
5156
Expectation.CspNotImplemented,
5257
Expectation.CspNotImplementedButReportingEnabled,
@@ -301,6 +306,7 @@ export function contentSecurityPolicyTest(
301306
);
302307

303308
// Check to see if the test passed or failed
309+
// If it passed, report any duplicate report-uri/report-to directives
304310
if (
305311
[
306312
expectation,
@@ -310,10 +316,17 @@ export function contentSecurityPolicyTest(
310316
].includes(output.result)
311317
) {
312318
output.pass = true;
319+
if (csp.has(DUPLICATE_WARNINGS_KEY)) {
320+
output.result = Expectation.CspImplementedButDuplicateDirectives;
321+
}
313322
}
314323

315324
output.data = {};
316325
for (const [key, value] of csp) {
326+
// filter out the duplicate warnings key from the parsed CSP
327+
if (key === DUPLICATE_WARNINGS_KEY) {
328+
continue;
329+
}
317330
output.data[key] = [...value].sort();
318331
}
319332

src/grader/charts.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,16 @@ export const SCORE_TABLE = new Map([
214214
</p>`,
215215
},
216216
],
217+
[
218+
Expectation.CspImplementedButDuplicateDirectives,
219+
{
220+
description: `<p>
221+
Content Security Policy (CSP) implemented, but contains duplicate directives.
222+
</p>`,
223+
modifier: 0,
224+
recommendation: `<p>Remove duplicate directives from the CSP</p>`,
225+
},
226+
],
217227

218228
// Cookies
219229
[

src/types.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ export const Expectation = {
106106
CspNotImplemented: "csp-not-implemented",
107107
CspNotImplementedButReportingEnabled:
108108
"csp-not-implemented-but-reporting-enabled",
109+
CspImplementedButDuplicateDirectives:
110+
"csp-implemented-but-duplicate-directives",
109111

110112
// SUBRESOURCE INTEGRITY
111113

test/csp-parser.test.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,28 @@ describe("Content Security Policy Parser", function () {
196196
}
197197
});
198198

199-
it("should parse this header correctly", function () {
199+
it("should parse this example header correctly", function () {
200200
let policy = [
201201
"default-src 'self' blob: https://*.cnn.com:* http://*.cnn.com:* *.cnn.io:* *.cnn.net:* *.turner.com:* *.turner.io:* *.ugdturner.com:* courageousstudio.com *.vgtf.net:*; script-src 'unsafe-eval' 'unsafe-inline' 'self' *; style-src 'unsafe-inline' 'self' blob: *; child-src 'self' blob: *; frame-src 'self' *; object-src 'self' *; img-src 'self' data: blob: *; media-src 'self' data: blob: *; font-src 'self' data: *; connect-src 'self' data: *; frame-ancestors 'self' https://*.cnn.com:* http://*.cnn.com https://*.cnn.io:* http://*.cnn.io:* *.turner.com:* courageousstudio.com;",
202202
];
203203
const res = parseCsp(policy);
204204
assert(res);
205205
});
206+
207+
it("should parse a policy with duplicate report-uri entries and report those duplicates", function () {
208+
let policy = [
209+
"report-uri https://www.dropbox.com/csp_log?policy_name=metaserver-whitelist ; report-uri https://www.dropbox.com/csp_log?policy_name=metaserver-dynamic",
210+
];
211+
const res = parseCsp(policy);
212+
assert(res);
213+
assert(res.has("_observatory_duplicate_key_warnings"));
214+
assert(res.get("_observatory_duplicate_key_warnings")?.has("report-uri"));
215+
});
216+
it("should parse a policy with duplicate report-to entries and report those duplicates", function () {
217+
let policy = ["report-to some_name ; report-to some_other_name"];
218+
const res = parseCsp(policy);
219+
assert(res);
220+
assert(res.has("_observatory_duplicate_key_warnings"));
221+
assert(res.get("_observatory_duplicate_key_warnings")?.has("report-to"));
222+
});
206223
});

0 commit comments

Comments
 (0)