Summary
checkCSP in src/rules.ts only penalizes unsafe-inline, unsafe-eval, and wildcards in default-src/script-src, but never checks whether base-uri is restricted. This matters because base-uri is one of the only CSP directives that does NOT fall back to default-src — meaning a policy of default-src 'self' leaves base-uri completely unrestricted. The tool currently awards 20/30 points (full CSP marks, "good" status) for exactly this policy, silently hiding a high-severity bypass.
Investigation Path
- Read
src/rules.ts lines 41–71 (checkCSP) and enumerated every deduction condition.
- Cross-referenced the CSP Level 2 specification and MDN to confirm
base-uri has no default-src fallback.
- Traced the README example (
default-src 'self') through the scoring logic to confirm it receives score 20/30, status "good", with zero findings or recommendations about base-uri.
- Consulted Google's CSP Evaluator and the OWASP CSP Cheat Sheet, both of which classify a missing
base-uri as HIGH severity.
Evidence
- File:
src/rules.ts (lines 41–71)
- Current behavior:
checkCSP awards 20/30 and status: 'good' for Content-Security-Policy: default-src 'self' — no finding or recommendation is emitted regarding base-uri.
- Expected behavior: If neither
base-uri 'none' nor base-uri 'self' is present in the policy, the function should emit a finding and deduct points, because default-src does not restrict base-uri.
- CSP spec reference: CSP Level 2 §6.1 — "The
base-uri directive restricts the URLs which can be used in a document's <base> element. … Unlike most other directives, base-uri does not fall back to default-src."
- OWASP reference: OWASP CSP Cheat Sheet — recommends
base-uri 'none' as a baseline directive.
- Google CSP Evaluator: rates the absence of a
base-uri restriction as a high-severity finding.
Reproducer
import { checkCSP } from './src/rules.js';
// The README's own recommended starting policy
const result = checkCSP({ 'content-security-policy': "default-src 'self'" });
console.log(result.score); // 20 ← full marks
console.log(result.status); // 'good'
console.log(result.findings); // [] ← no mention of base-uri at all
A developer following this tool's guidance would ship a site where an attacker capable of injecting one HTML element — <base href="https://attacker.com"> — redirects every relative URL resource load to an attacker-controlled origin. Script tags that use relative paths like <script src="/app.js"> now load from https://attacker.com/app.js, silently bypassing script-src 'self'.
Impact
Every developer who runs this tool against a site with a CSP of default-src 'self' (or any policy without base-uri) receives a "good" score with no remediation guidance. This is the exact policy the README recommends as a starting point. In practice:
- Sites with server-side template rendering (Rails ERB, Django templates, Jinja2, Blade) that allow any user-controlled attribute output are at risk.
- The base-injection attack bypasses
script-src 'self' for any relative-path script loads without requiring JavaScript execution — it works even when unsafe-inline is blocked.
- Because the tool gives a green light, operators have no reason to add
base-uri 'none' and the gap persists silently in production.
Suggested Remediation
Add a base-uri check inside checkCSP in src/rules.ts:
// After the existing unsafe-inline / unsafe-eval / wildcard checks:
const hasBaseUri = /base-uri\s+/i.test(raw);
const hasRestrictiveBaseUri = /base-uri\s+(?:'none'|'self')/i.test(raw);
if (!hasBaseUri || !hasRestrictiveBaseUri) {
score -= 5;
findings.push("'base-uri' is not restricted — <base> injection can redirect relative-URL resource loads");
recommendations.push("Add base-uri 'none' (or base-uri 'self') to prevent <base> tag injection");
}
Adjust the deduction weight to match the team's scoring philosophy. The maxScore for CSP should be increased correspondingly if the intent is to reward base-uri presence without penalizing existing perfect scores.
Acceptance Criteria
Auto-generated by Hermes project review agent · 2026-05-26T00:22:00Z
Summary
checkCSPinsrc/rules.tsonly penalizesunsafe-inline,unsafe-eval, and wildcards indefault-src/script-src, but never checks whetherbase-uriis restricted. This matters becausebase-uriis one of the only CSP directives that does NOT fall back todefault-src— meaning a policy ofdefault-src 'self'leavesbase-uricompletely unrestricted. The tool currently awards 20/30 points (full CSP marks,"good"status) for exactly this policy, silently hiding a high-severity bypass.Investigation Path
src/rules.tslines 41–71 (checkCSP) and enumerated every deduction condition.base-urihas nodefault-srcfallback.default-src 'self') through the scoring logic to confirm it receives score 20/30, status"good", with zero findings or recommendations aboutbase-uri.base-urias HIGH severity.Evidence
src/rules.ts(lines 41–71)checkCSPawards 20/30 andstatus: 'good'forContent-Security-Policy: default-src 'self'— no finding or recommendation is emitted regardingbase-uri.base-uri 'none'norbase-uri 'self'is present in the policy, the function should emit a finding and deduct points, becausedefault-srcdoes not restrictbase-uri.base-uridirective restricts the URLs which can be used in a document's<base>element. … Unlike most other directives,base-uridoes not fall back todefault-src."base-uri 'none'as a baseline directive.base-urirestriction as a high-severity finding.Reproducer
A developer following this tool's guidance would ship a site where an attacker capable of injecting one HTML element —
<base href="https://attacker.com">— redirects every relative URL resource load to an attacker-controlled origin. Script tags that use relative paths like<script src="/app.js">now load fromhttps://attacker.com/app.js, silently bypassingscript-src 'self'.Impact
Every developer who runs this tool against a site with a CSP of
default-src 'self'(or any policy withoutbase-uri) receives a"good"score with no remediation guidance. This is the exact policy the README recommends as a starting point. In practice:script-src 'self'for any relative-path script loads without requiring JavaScript execution — it works even whenunsafe-inlineis blocked.base-uri 'none'and the gap persists silently in production.Suggested Remediation
Add a
base-uricheck insidecheckCSPinsrc/rules.ts:Adjust the deduction weight to match the team's scoring philosophy. The
maxScorefor CSP should be increased correspondingly if the intent is to rewardbase-uripresence without penalizing existing perfect scores.Acceptance Criteria
checkCSP({ 'content-security-policy': "default-src 'self'" })emits a finding referencingbase-uriand does not returnstatus: 'good'checkCSP({ 'content-security-policy': "default-src 'self'; base-uri 'none'" })returns nobase-uri-related findingcheckCSP({ 'content-security-policy': "default-src 'self'; base-uri 'self'" })returns nobase-uri-related findingtest/analyzer.test.tscovering the missing and presentbase-uriscenariosbase-uri 'none'Auto-generated by Hermes project review agent · 2026-05-26T00:22:00Z