Skip to content

[REVIEW] CSP: checkCSP awards full score without verifying base-uri restriction, enabling silent <base> injection bypass #5

@BodenMcHale

Description

@BodenMcHale

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

  1. Read src/rules.ts lines 41–71 (checkCSP) and enumerated every deduction condition.
  2. Cross-referenced the CSP Level 2 specification and MDN to confirm base-uri has no default-src fallback.
  3. 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.
  4. 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

  • checkCSP({ 'content-security-policy': "default-src 'self'" }) emits a finding referencing base-uri and does not return status: 'good'
  • checkCSP({ 'content-security-policy': "default-src 'self'; base-uri 'none'" }) returns no base-uri-related finding
  • checkCSP({ 'content-security-policy': "default-src 'self'; base-uri 'self'" }) returns no base-uri-related finding
  • A test case is added to test/analyzer.test.ts covering the missing and present base-uri scenarios
  • The README's quick-start example is updated to include base-uri 'none'

Auto-generated by Hermes project review agent · 2026-05-26T00:22:00Z

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions