Skip to content

Commit 5ba88ce

Browse files
redmoddclaude
andcommitted
fix: reject CSP override sources that could break out of the meta attribute
The source allowlist blocked the CSP separators but not " < >, so a source could escape the content="..." attribute and inject markup. Exclude those chars too, and drop the now-redundant cast in cspMeta. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1695e6d commit 5ba88ce

3 files changed

Lines changed: 24 additions & 5 deletions

File tree

packages/tessera-learn/src/plugin/csp.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ export const WEB_CSP_BASELINE: Record<string, string[]> = {
1919
};
2020

2121
// Reject the separators (whitespace ends a source, ';' ends a directive, ','
22-
// ends a policy) so a stray char can't inject directives when sources are joined.
22+
// ends a policy) so a stray char can't inject directives when sources are joined,
23+
// plus " < > so a source can't break out of the content="..." meta attribute.
2324
const CSP_DIRECTIVE = /^[a-zA-Z][a-zA-Z-]*$/;
24-
const CSP_SOURCE = /^[^\s;,]+$/;
25+
const CSP_SOURCE = /^[^\s;,"<>]+$/;
2526

2627
export function isCspOverrides(v: unknown): v is Record<string, string[]> {
2728
return (

packages/tessera-learn/src/plugin/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,7 @@ function readExportStandard(read: CourseConfigRead): string {
278278
// `false` drops the meta for deployments that set a CSP header themselves.
279279
function cspMeta(read: CourseConfigRead): string {
280280
if (readExportStandard(read) !== 'web') return '';
281-
const csp = read.ok
282-
? (read.config.export as { csp?: unknown } | undefined)?.csp
283-
: undefined;
281+
const csp = read.ok ? read.config.export?.csp : undefined;
284282
if (csp === false) return '';
285283
return `\n <meta http-equiv="Content-Security-Policy" content="${buildCsp(csp)}" />`;
286284
}

packages/tessera-learn/tests/csp.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,25 @@ describe('isCspOverrides', () => {
6363
expect(isCspOverrides({ 'font-src': ["'self' https://x"] })).toBe(false);
6464
expect(isCspOverrides({ '': ["'self'"] })).toBe(false);
6565
expect(isCspOverrides({ 'font src': ["'self'"] })).toBe(false);
66+
// A " or > would break out of the content="..." meta attribute into markup.
67+
expect(
68+
isCspOverrides({ 'font-src': ['https://x"><script>alert(1)</script>'] }),
69+
).toBe(false);
70+
});
71+
72+
it('accepts the legitimate source forms', () => {
73+
for (const src of [
74+
"'self'",
75+
"'unsafe-inline'",
76+
"'none'",
77+
'https:',
78+
'data:',
79+
'blob:',
80+
'https://fonts.gstatic.com',
81+
"'nonce-abc123=='",
82+
"'sha256-AbC+/dEf='",
83+
]) {
84+
expect(isCspOverrides({ 'font-src': [src] })).toBe(true);
85+
}
6686
});
6787
});

0 commit comments

Comments
 (0)