Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ interface HeaderFinding {
| Header | Max Points | Key Checks |
|---|---|---|
| Strict-Transport-Security | 20 | max-age ≥ 1 year, includeSubDomains, preload |
| Content-Security-Policy | 30 | presence, no unsafe-inline/eval, no wildcards, form-action set |
| Content-Security-Policy | 30 | presence, no unsafe-inline/eval, no wildcards, form-action/base-uri/object-src fallback set |
| X-Frame-Options | 15 | DENY or SAMEORIGIN (or CSP frame-ancestors) |
| X-Content-Type-Options | 10 | nosniff |
| Referrer-Policy | 10 | strict values only |
Expand Down
10 changes: 10 additions & 0 deletions src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ export function checkCSP(headers: RawHeaders): HeaderFinding {
findings.push("No base-uri directive — <base> injection can redirect relative nonce sources (base-uri does not inherit from default-src)");
recommendations.push("Add base-uri 'self' or base-uri 'none' to prevent <base> injection");
}
// object-src DOES inherit from default-src, but only when default-src is
// actually set. If neither is present, <object>/<embed> plugin content is
// completely unrestricted — a legacy but still-audited XSS vector — and every
// other fetch directive not explicitly listed (img-src, media-src, connect-src,
// etc.) also silently defaults to allow-all with no default-src to fall back to.
if (extractCspDirective(raw, 'default-src') === undefined && extractCspDirective(raw, 'object-src') === undefined) {
score -= 2;
findings.push('No default-src or object-src directive — plugin content (<object>/<embed>) is unrestricted, and any other fetch directive not explicitly listed defaults to allow-all');
recommendations.push("Add object-src 'none' (or set default-src as a fallback that covers it)");
}
score = Math.max(5, score); // at least 5 for having any CSP

return { header: 'Content-Security-Policy', score, maxScore: 30, status: findings.length === 0 ? 'good' : 'warning', raw, findings, recommendations };
Expand Down
21 changes: 20 additions & 1 deletion test/analyzer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,10 @@ describe('checkCSP', () => {
});

it("does not penalize 'unsafe-inline' when 'strict-dynamic' + nonce present", () => {
// 20 - 2 (no default-src or object-src) = 18
const r = checkCSP({ 'content-security-policy': "script-src 'strict-dynamic' 'nonce-abc123' 'unsafe-inline' https://example.com; form-action 'self'; base-uri 'none'" });
expect(r.findings.some(f => f.includes('unsafe-inline'))).toBe(false);
expect(r.score).toBe(20);
expect(r.score).toBe(18);
});

it("still penalizes 'unsafe-inline' when 'strict-dynamic' present without nonce/hash", () => {
Expand Down Expand Up @@ -263,6 +264,24 @@ describe('checkCSP', () => {
expect(r.findings.some(f => /base-uri/i.test(f))).toBe(false);
expect(r.score).toBe(20);
});

it('flags a policy with neither default-src nor object-src', () => {
const r = checkCSP({ 'content-security-policy': "script-src 'self'; form-action 'self'; base-uri 'self'" });
expect(r.findings.some(f => /object-src/i.test(f))).toBe(true);
expect(r.status).toBe('warning');
expect(r.score).toBe(18);
});

it('default-src alone satisfies the object-src fallback check', () => {
const r = checkCSP({ 'content-security-policy': "default-src 'self'; form-action 'self'; base-uri 'self'" });
expect(r.findings.some(f => /object-src/i.test(f))).toBe(false);
});

it("object-src 'none' satisfies the check even without default-src", () => {
const r = checkCSP({ 'content-security-policy': "script-src 'self'; object-src 'none'; form-action 'self'; base-uri 'self'" });
expect(r.findings.some(f => /object-src/i.test(f))).toBe(false);
expect(r.score).toBe(20);
});
});

describe('checkXFrameOptions', () => {
Expand Down
Loading