Class: Otto::Security::CSP::Writer
- Inherits:
-
Object
- Object
- Otto::Security::CSP::Writer
- Defined in:
- lib/otto/security/csp/writer.rb
Overview
The single structural apply core for nonce-based Content-Security-Policy emission. Every in-framework surface that writes a nonce CSP onto a response — Response#apply_csp, EmitMiddleware, and the deprecated Response#send_csp_headers shim — routes through Writer.apply, so the emission invariants are properties of ONE method rather than guard logic re-implemented (and re-reviewed) at each surface:
- Enabled only. No header unless the security config has nonce-CSP on.
- Nonce present. A nil/empty nonce never produces a broken
script-src 'nonce-'policy; it skips. - HTML only. Non-HTML responses (JSON, redirects, static assets) are left untouched.
- Passive layers never clobber. In
:backstopmode an existing CSP is deferred to; only an explicit:overridereplaces one.
Writes are in-place and key-scoped: Writer.apply finds any case-variant of
the CSP key (Rack 3 mandates lowercase response-header keys, but a
canonical-/mixed-cased key from a downstream layer is a spec violation this
corrects in place), deletes it, and writes the lowercase key into the
CALLER’S headers hash. There is no wrapping, no copy, and no
“callers-must-use-the-return-value” contract — the [status, headers,
body] tuple never needs reassignment. A frozen headers hash therefore
fails loud (FrozenError) on write, surfacing the downstream SPEC violation
rather than silently dropping the policy.
The return is a Result, not the headers: result.applied?,
result.policy, result.skip_reason give uniform observability across
every surface (and drive the optional debug log) without any cleverness to
detect “did anything happen”.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- CSP_HEADER =
Canonical (lowercase, per Rack 3 SPEC) response-header keys.
'content-security-policy'- CONTENT_TYPE_HEADER =
'content-type'- MODES =
Emission modes.
:overrideis a deliberate per-request call that REPLACES any existing CSP (the caller owns this response’s policy).:backstopis a passive layer that DEFERS to an existing CSP (it only fills the gap, never clobbers). %i[override backstop].freeze
Class Method Summary collapse
-
.apply(headers, nonce, config:, mode: :override, development_mode: false) ⇒ Result
Apply a nonce-based CSP to the caller’s response headers, in place.
Class Method Details
.apply(headers, nonce, config:, mode: :override, development_mode: false) ⇒ Result
Apply a nonce-based CSP to the caller’s response headers, in place.
102 103 104 105 106 107 108 109 110 |
# File 'lib/otto/security/csp/writer.rb', line 102 def self.apply(headers, nonce, config:, mode: :override, development_mode: false) unless MODES.include?(mode) raise ArgumentError, "mode must be one of #{MODES.join(', ')}, got #{mode.inspect}" end result = evaluate(headers, nonce, config, mode, development_mode) log_debug(config, result) result end |