Skip to content

fix(csp): allow duplicate report-* directives #151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions scan

This file was deleted.

25 changes: 20 additions & 5 deletions src/analyzer/cspParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const DIRECTIVES_DISALLOWED_IN_META = [
"report-uri",
"sandbox",
];
const ALLOWED_DUPLICATE_KEYS = new Set(["report-uri", "report-to"]);
export const DUPLICATE_WARNINGS_KEY = "_observatory_duplicate_key_warnings";

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

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

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

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

// While technically valid in that you just use the first entry, we are saying that repeated
// directives are invalid so that people notice it
// directives are invalid so that people notice it. The exception are duplicate report-uri
// and report-to directives, which we allow.
if (directiveSeenBeforeThisPolicy.has(directive)) {
throw new Error(
`Duplicate directive ${directive} in policy ${policyIndex}`
);
if (ALLOWED_DUPLICATE_KEYS.has(directive)) {
duplicate_warnings.add(directive);
} else {
throw new Error(
`Duplicate directive ${directive} in policy ${policyIndex}`
);
}
} else {
directiveSeenBeforeThisPolicy.add(directive);
}
Expand Down Expand Up @@ -146,7 +159,9 @@ export function parseCsp(cspList) {
: new Set(["'none'"]),
])
);

if (duplicate_warnings.size) {
finalCsp.set(DUPLICATE_WARNINGS_KEY, duplicate_warnings);
}
return finalCsp;
}

Expand Down
15 changes: 14 additions & 1 deletion src/analyzer/tests/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import {
} from "../../headers.js";
import { Requests, Policy, BaseOutput } from "../../types.js";
import { Expectation } from "../../types.js";
import { parseCsp, parseCspMeta } from "../cspParser.js";
import {
DUPLICATE_WARNINGS_KEY,
parseCsp,
parseCspMeta,
} from "../cspParser.js";
import { getHttpHeaders } from "../utils.js";

const DANGEROUSLY_BROAD = new Set([
Expand Down Expand Up @@ -47,6 +51,7 @@ export class CspOutput extends BaseOutput {
Expectation.CspImplementedWithUnsafeEval,
Expectation.CspImplementedWithUnsafeInline,
Expectation.CspImplementedWithInsecureScheme,
Expectation.CspImplementedButDuplicateDirectives,
Expectation.CspHeaderInvalid,
Expectation.CspNotImplemented,
Expectation.CspNotImplementedButReportingEnabled,
Expand Down Expand Up @@ -301,6 +306,7 @@ export function contentSecurityPolicyTest(
);

// Check to see if the test passed or failed
// If it passed, report any duplicate report-uri/report-to directives
if (
[
expectation,
Expand All @@ -310,10 +316,17 @@ export function contentSecurityPolicyTest(
].includes(output.result)
) {
output.pass = true;
if (csp.has(DUPLICATE_WARNINGS_KEY)) {
output.result = Expectation.CspImplementedButDuplicateDirectives;
}
}

output.data = {};
for (const [key, value] of csp) {
// filter out the duplicate warnings key from the parsed CSP
if (key === DUPLICATE_WARNINGS_KEY) {
continue;
}
output.data[key] = [...value].sort();
}

Expand Down
10 changes: 10 additions & 0 deletions src/grader/charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ export const SCORE_TABLE = new Map([
</p>`,
},
],
[
Expectation.CspImplementedButDuplicateDirectives,
{
description: `<p>
Content Security Policy (CSP) implemented, but contains duplicate directives.
</p>`,
modifier: 0,
recommendation: `<p>Remove duplicate directives from the CSP</p>`,
},
],

// Cookies
[
Expand Down
2 changes: 2 additions & 0 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export const Expectation = {
CspNotImplemented: "csp-not-implemented",
CspNotImplementedButReportingEnabled:
"csp-not-implemented-but-reporting-enabled",
CspImplementedButDuplicateDirectives:
"csp-implemented-but-duplicate-directives",

// SUBRESOURCE INTEGRITY

Expand Down
19 changes: 18 additions & 1 deletion test/csp-parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,28 @@ describe("Content Security Policy Parser", function () {
}
});

it("should parse this header correctly", function () {
it("should parse this example header correctly", function () {
let policy = [
"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;",
];
const res = parseCsp(policy);
assert(res);
});

it("should parse a policy with duplicate report-uri entries and report those duplicates", function () {
let policy = [
"report-uri https://www.dropbox.com/csp_log?policy_name=metaserver-whitelist ; report-uri https://www.dropbox.com/csp_log?policy_name=metaserver-dynamic",
];
const res = parseCsp(policy);
assert(res);
assert(res.has("_observatory_duplicate_key_warnings"));
assert(res.get("_observatory_duplicate_key_warnings")?.has("report-uri"));
});
it("should parse a policy with duplicate report-to entries and report those duplicates", function () {
let policy = ["report-to some_name ; report-to some_other_name"];
const res = parseCsp(policy);
assert(res);
assert(res.has("_observatory_duplicate_key_warnings"));
assert(res.get("_observatory_duplicate_key_warnings")?.has("report-to"));
});
});
Loading