Module: Otto::Security::CSP::Policy

Defined in:
lib/otto/security/csp/policy.rb

Overview

Assembles Content-Security-Policy strings from Otto’s directive sets and the optional reporting directives.

This is the policy-BUILDING half of Otto’s CSP support, extracted from Otto::Security::Config so the domain (directive sets, report-uri / report-to assembly) lives beside the parser and the middlewares under Otto::Security::CSP. Otto::Security::Config keeps thin delegating facades (Otto::Security::Config#generate_nonce_csp and its static counterpart), so callers and output are unchanged — the assembly logic simply has a home of its own now.

All methods are pure functions of their arguments (the report URI/URL are passed in, not read from global state), so the same policy string can be produced from any surface without a Config in hand.

Constant Summary collapse

REPORTING_GROUP =

Endpoint group name shared by the CSP report-to directive and the Reporting-Endpoints response header (modern Reporting API). Browsers match the directive’s group to the header’s key, so both must agree. Otto::Security::Config::CSP_REPORTING_GROUP aliases this so the two can never drift.

'otto-csp'

Class Method Summary collapse

Class Method Details

.development_directives(nonce) ⇒ Array<String>

CSP directives for the development environment.

Development mode allows inline scripts/styles and hot reloading connections for better developer experience with build tools like Vite.

Parameters:

  • nonce (String)

    nonce value injected into script-src

Returns:

  • (Array<String>)

    directive strings, each terminated with ;



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/otto/security/csp/policy.rb', line 98

def development_directives(nonce)
  [
    "default-src 'none';",
    "script-src 'nonce-#{nonce}' 'unsafe-inline';", # Allow inline scripts for development tools
    "style-src 'self' 'unsafe-inline';",
    "connect-src 'self' ws: wss: http: https:;", # Allow HTTP and all WebSocket connections for dev tools
    "img-src 'self' data:;",
    "font-src 'self';",
    "object-src 'none';",
    "base-uri 'self';",
    "form-action 'self';",
    "frame-ancestors 'none';",
    "manifest-src 'self';",
    "worker-src 'self' data:;",
  ]
end

.nonce_policy(nonce, development_mode: false, report_uri: nil, report_to_url: nil) ⇒ String

Build the per-request nonce CSP policy string.

Byte-identical to Otto’s historical Otto::Security::Config#generate_nonce_csp output: the base directive set (development or production) followed by the optional report-uri and report-to directives, each terminated with ; and joined by a single space.

Parameters:

  • nonce (String)

    nonce value injected into script-src

  • development_mode (Boolean) (defaults to: false)

    use the development directive set

  • report_uri (String, nil) (defaults to: nil)

    path for the report-uri directive (omitted when nil/empty)

  • report_to_url (String, nil) (defaults to: nil)

    absolute URL configured for the modern Reporting API; its presence (not its value) toggles the report-to <group> directive (omitted when nil/empty)

Returns:

  • (String)

    complete CSP policy string



47
48
49
50
51
52
53
54
# File 'lib/otto/security/csp/policy.rb', line 47

def nonce_policy(nonce, development_mode: false, report_uri: nil, report_to_url: nil)
  directives = development_mode ? development_directives(nonce) : production_directives(nonce)
  uri_directive = report_uri_directive(report_uri)
  to_directive  = report_to_directive(report_to_url)
  directives += ["#{uri_directive};"] if uri_directive
  directives += ["#{to_directive};"] if to_directive
  directives.join(' ')
end

.production_directives(nonce) ⇒ Array<String>

CSP directives for the production environment.

Production mode is more restrictive, only allowing HTTPS connections and nonce-only scripts for enhanced XSS protection.

Parameters:

  • nonce (String)

    nonce value injected into script-src

Returns:

  • (Array<String>)

    directive strings, each terminated with ;



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/otto/security/csp/policy.rb', line 122

def production_directives(nonce)
  [
    "default-src 'none';",                     # Restrict to same origin by default
    "script-src 'nonce-#{nonce}';",            # Only allow scripts with valid nonce
    "style-src 'self' 'unsafe-inline';",       # Allow inline styles and same-origin stylesheets
    "connect-src 'self' wss: https:;",         # Only HTTPS and secure WebSockets
    "img-src 'self' data:;",                   # Allow images from same origin and data URIs
    "font-src 'self';",                        # Allow fonts from same origin only
    "object-src 'none';",                      # Block <object>, <embed>, and <applet> elements
    "base-uri 'self';",                        # Restrict <base> tag targets to same origin
    "form-action 'self';",                     # Restrict form submissions to same origin
    "frame-ancestors 'none';",                 # Prevent site from being embedded in frames
    "manifest-src 'self';",                    # Allow web app manifests from same origin
    "worker-src 'self' data:;",                # Allow Workers from same origin and data blobs
  ]
end

.report_to_directive(url) ⇒ String?

The report-to directive (modern Reporting API), or nil when no reporting endpoint URL is configured. Its group name matches the Reporting-Endpoints header. No trailing semicolon.

Parameters:

  • url (String, nil)

Returns:

  • (String, nil)


85
86
87
88
89
# File 'lib/otto/security/csp/policy.rb', line 85

def report_to_directive(url)
  return nil if url.nil? || url.empty?

  "report-to #{REPORTING_GROUP}"
end

.report_uri_directive(uri) ⇒ String?

The report-uri directive, or nil when no report URI is configured. No trailing semicolon (callers add their own separator).

Parameters:

  • uri (String, nil)

Returns:

  • (String, nil)


73
74
75
76
77
# File 'lib/otto/security/csp/policy.rb', line 73

def report_uri_directive(uri)
  return nil if uri.nil? || uri.empty?

  "report-uri #{uri}"
end

.static_policy(base, report_uri: nil, report_to_url: nil) ⇒ String

Build a static CSP header value: a base policy plus the optional reporting directives, joined '; '. Byte-identical to the bare policy when no reporting is configured.

Parameters:

  • base (String)

    the base policy (e.g. from Otto::Security::Config#enable_csp!)

  • report_uri (String, nil) (defaults to: nil)

    path for the report-uri directive

  • report_to_url (String, nil) (defaults to: nil)

    absolute URL toggling report-to

Returns:

  • (String)


64
65
66
# File 'lib/otto/security/csp/policy.rb', line 64

def static_policy(base, report_uri: nil, report_to_url: nil)
  [base, report_uri_directive(report_uri), report_to_directive(report_to_url)].compact.join('; ')
end