Module: Otto::Security::Core

Included in:
Otto
Defined in:
lib/otto/security/core.rb

Overview

Core security configuration methods included in the Otto class. Provides the public API for enabling and configuring security features.

Instance Method Summary collapse

Instance Method Details

#add_auth_strategy(name, strategy) ⇒ Object

Add an authentication strategy with a registered name

This is the primary public API for registering authentication strategies. The name you provide here will be available as strategy_result.strategy_name in your application code, making it easy to identify which strategy authenticated the current request.

Also available via Otto::Security::Configurator for consolidated security config.

Examples:

otto.add_auth_strategy('session', SessionStrategy.new(session_key: 'user_id'))
otto.add_auth_strategy('api_key', APIKeyStrategy.new)

Parameters:

  • name (String, Symbol)

    Strategy name (e.g., ‘session’, ‘api_key’, ‘jwt’)

  • strategy (AuthStrategy)

    Strategy instance

Raises:

  • (ArgumentError)

    if strategy name already registered



247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/otto/security/core.rb', line 247

def add_auth_strategy(name, strategy)
  ensure_not_frozen!
  # Ensure auth_config is initialized (handles edge case where it might be nil)
  @auth_config = { auth_strategies: {}, default_auth_strategy: 'noauth' } if @auth_config.nil?

  # Strict mode: Detect strategy name collisions
  if @auth_config[:auth_strategies].key?(name)
    raise ArgumentError, "Authentication strategy '#{name}' is already registered"
  end

  @auth_config[:auth_strategies][name] = strategy
end

#add_rate_limit_rule(name, options) ⇒ Object

Add a custom rate limiting rule.

Examples:

otto.add_rate_limit_rule('uploads', limit: 5, period: 300, condition: ->(req) { req.post? && req.path.include?('upload') })

Parameters:

  • name (String, Symbol)

    Rule name

  • options (Hash)

    Rule configuration

Options Hash (options):

  • :limit (Integer)

    Maximum requests

  • :period (Integer)

    Time period in seconds (default: 60)

  • :condition (Proc)

    Optional condition proc that receives request



62
63
64
65
# File 'lib/otto/security/core.rb', line 62

def add_rate_limit_rule(name, options)
  ensure_not_frozen!
  @security_config.rate_limiting_config[:custom_rules][name.to_s] = options
end

#add_trusted_proxy(proxy) ⇒ Object

Add a trusted proxy server for accurate client IP detection. Only requests from trusted proxies will have their forwarded headers honored.

Examples:

otto.add_trusted_proxy('10.0.0.0/8')
otto.add_trusted_proxy(/^172\.16\./)

Parameters:

  • proxy (String, Regexp)

    IP address, CIDR range, or regex pattern



74
75
76
77
# File 'lib/otto/security/core.rb', line 74

def add_trusted_proxy(proxy)
  ensure_not_frozen!
  @security_config.add_trusted_proxy(proxy)
end

#enable_csp!(policy = "default-src 'self'") ⇒ Object

Enable Content Security Policy (CSP) header to prevent XSS attacks. The default policy only allows resources from the same origin.

Examples:

otto.enable_csp!("default-src 'self'; script-src 'self' 'unsafe-inline'")

Parameters:

  • policy (String) (defaults to: "default-src 'self'")

    CSP policy string (default: “default-src ‘self’”)



112
113
114
115
# File 'lib/otto/security/core.rb', line 112

def enable_csp!(policy = "default-src 'self'")
  ensure_not_frozen!
  @security_config.enable_csp!(policy)
end

#enable_csp_emission!(eager: false, development_mode: nil) ⇒ void

This method returns an undefined value.

Mount Otto::Security::CSP::EmitMiddleware so nonce-based CSP headers are applied to responses by the framework instead of hand-rolled in each app.

It is a passive backstop: it emits a nonce CSP only for responses that would otherwise ship without one, and never clobbers a policy a route already set. Enable nonce-CSP via #enable_csp_with_nonce! for it to emit anything — until then the middleware is INERT (a transparent pass-through), NOT an error. The two may be enabled in either order: both read the same security config, so mounting the backstop first and enabling nonce-CSP later works. Enable-order independence is why this does not raise when nonce-CSP is off.

By DEFAULT it is emit-if-consumed — it emits only when the request actually consumed a nonce (a view called Request#csp_nonce). This is the only safe blanket default: a nonce-only policy on a page that never stamped the nonce blocks every script.

Examples:

otto.enable_csp_with_nonce!
otto.enable_csp_emission!(development_mode: -> (env) { ENV['RACK_ENV'] == 'development' })

Parameters:

  • eager (Boolean) (defaults to: false)

    mint-and-emit for every eligible HTML response rather than only emit-if-consumed (see the middleware’s caveats)

  • development_mode (Boolean, #call, nil) (defaults to: nil)

    whether to emit development directives; a callable is evaluated per request with the env



163
164
165
166
167
168
# File 'lib/otto/security/core.rb', line 163

def enable_csp_emission!(eager: false, development_mode: nil)
  ensure_not_frozen!
  return if @middleware.includes?(Otto::Security::CSP::EmitMiddleware)

  @middleware.add(Otto::Security::CSP::EmitMiddleware, eager: eager, development_mode: development_mode)
end

#enable_csp_reporting!(report_uri, endpoint_url: nil) {|report| ... } ⇒ void

This method returns an undefined value.

Enable turnkey Content Security Policy violation reporting.

This is the receiving half of Otto’s CSP support. It: 1. Configures the report path (config.csp_report_uri = report_uri), so a report-uri directive is appended to every emitted CSP policy (static #enable_csp! and nonce #enable_csp_with_nonce! alike). 2. Registers your violation callback (if a block is given). 3. Injects Otto::Security::CSP::ReportMiddleware so browser POSTs to the report path are received, parsed, and dispatched to the callback — always answered with 204 and never touching your routes.

The middleware is pinned to run OUTERMOST (ahead of CSRF and every other middleware), so it short-circuits report POSTs before CSRF validation — browsers can post reports without a CSRF token. This holds regardless of the order in which you enable security features.

SECURITY / DoS: running outermost also means the receiver sits ahead of rate limiting (rate limiting is inner middleware). This is intentional — a public, unauthenticated report endpoint cannot depend on CSRF, session, or per-client throttling state — but it means a client can POST reports up to the 64 KiB body cap and invoke your callback on each one. Keep the callback cheap and bounded (sample or aggregate; avoid unbounded synchronous I/O), and put request-rate control for this path at the edge (reverse proxy / CDN / WAF) rather than expecting Otto to throttle it.

To (re)assign the callback later without touching the wiring, use the config primitive directly: otto.security_config.on_csp_violation { ... }.

For modern browsers (which have deprecated report-uri), also pass endpoint_url: — an ABSOLUTE URL whose path is report_uri. Otto then emits a report-to directive plus a Reporting-Endpoints header so those browsers deliver application/reports+json to the same receiver.

Examples:

otto.enable_csp_with_nonce!
otto.enable_csp_reporting!('/_/csp-report',
  endpoint_url: 'https://example.com/_/csp-report') do |report|
  Otto.logger.warn("CSP violation: #{report.to_h}")
end

Parameters:

  • report_uri (String)

    path browsers POST reports to (matched against PATH_INFO, e.g. /_/csp-report).

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

    absolute URL for the modern Reporting API endpoint (e.g. https://example.com/_/csp-report); nil emits only the legacy report-uri.

Yield Parameters:



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/otto/security/core.rb', line 216

def enable_csp_reporting!(report_uri, endpoint_url: nil, &block)
  ensure_not_frozen!

  @security_config.csp_report_uri = report_uri
  @security_config.csp_report_to_url = endpoint_url unless endpoint_url.nil?
  @security_config.on_csp_violation(&block) if block

  return if @middleware.includes?(Otto::Security::CSP::ReportMiddleware)

  # Pin OUTERMOST so it intercepts report POSTs ahead of CSRF regardless of
  # the order security features are enabled in. add_with_position fires the
  # stack's on_change callback, which rebuilds @app (wired in
  # Otto#initialize_core_state) — no explicit build_app! needed.
  @middleware.add_with_position(Otto::Security::CSP::ReportMiddleware, position: :outermost)
end

#enable_csp_with_nonce!(debug: false) ⇒ Object

Enable Content Security Policy (CSP) with nonce support for dynamic header generation. This enables the res.send_csp_headers response helper method.

Examples:

otto.enable_csp_with_nonce!(debug: true)

Parameters:

  • debug (Boolean) (defaults to: false)

    Enable debug logging for CSP headers (default: false)



133
134
135
136
# File 'lib/otto/security/core.rb', line 133

def enable_csp_with_nonce!(debug: false)
  ensure_not_frozen!
  @security_config.enable_csp_with_nonce!(debug: debug)
end

#enable_csrf_protection!Object

Enable CSRF protection for POST, PUT, DELETE, and PATCH requests. This will automatically add CSRF tokens to HTML forms and validate them on unsafe HTTP methods.

Examples:

otto.enable_csrf_protection!


16
17
18
19
20
21
22
# File 'lib/otto/security/core.rb', line 16

def enable_csrf_protection!
  ensure_not_frozen!
  return if @middleware.includes?(Otto::Security::Middleware::CSRFMiddleware)

  @security_config.enable_csrf_protection!
  use Otto::Security::Middleware::CSRFMiddleware
end

#enable_frame_protection!(option = 'SAMEORIGIN') ⇒ Object

Enable X-Frame-Options header to prevent clickjacking attacks.

Examples:

otto.enable_frame_protection!('DENY')

Parameters:

  • option (String) (defaults to: 'SAMEORIGIN')

    Frame options: ‘DENY’, ‘SAMEORIGIN’, or ‘ALLOW-FROM uri’



122
123
124
125
# File 'lib/otto/security/core.rb', line 122

def enable_frame_protection!(option = 'SAMEORIGIN')
  ensure_not_frozen!
  @security_config.enable_frame_protection!(option)
end

#enable_hsts!(max_age: 31_536_000, include_subdomains: true) ⇒ Object

Enable HTTP Strict Transport Security (HSTS) header. WARNING: This can make your domain inaccessible if HTTPS is not properly configured. Only enable this when you’re certain HTTPS is working correctly.

Examples:

otto.enable_hsts!(max_age: 86400, include_subdomains: false)

Parameters:

  • max_age (Integer) (defaults to: 31_536_000)

    Maximum age in seconds (default: 1 year)

  • include_subdomains (Boolean) (defaults to: true)

    Apply to all subdomains (default: true)



101
102
103
104
# File 'lib/otto/security/core.rb', line 101

def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
  ensure_not_frozen!
  @security_config.enable_hsts!(max_age: max_age, include_subdomains: include_subdomains)
end

#enable_rate_limiting!(options = {}) ⇒ Object

Enable rate limiting to protect against abuse and DDoS attacks. This will automatically add rate limiting rules based on client IP.

Examples:

otto.enable_rate_limiting!(requests_per_minute: 50)

Parameters:

  • options (Hash) (defaults to: {})

    Rate limiting configuration options

Options Hash (options):

  • :requests_per_minute (Integer)

    Maximum requests per minute per IP (default: 100)

  • :custom_rules (Hash)

    Custom rate limiting rules



45
46
47
48
49
50
51
# File 'lib/otto/security/core.rb', line 45

def enable_rate_limiting!(options = {})
  ensure_not_frozen!
  return if @middleware.includes?(Otto::Security::Middleware::RateLimitMiddleware)

  @security.configure_rate_limiting(options)
  use Otto::Security::Middleware::RateLimitMiddleware
end

#enable_request_validation!Object

Enable request validation including input sanitization, size limits, and protection against XSS and SQL injection attacks.

Examples:

otto.enable_request_validation!


29
30
31
32
33
34
35
# File 'lib/otto/security/core.rb', line 29

def enable_request_validation!
  ensure_not_frozen!
  return if @middleware.includes?(Otto::Security::Middleware::ValidationMiddleware)

  @security_config.input_validation = true
  use Otto::Security::Middleware::ValidationMiddleware
end

#set_security_headers(headers) ⇒ Object

Set custom security headers that will be added to all responses. These merge with the default security headers.

Examples:

otto.set_security_headers({
  'content-security-policy' => "default-src 'self'",
  'strict-transport-security' => 'max-age=31536000'
})

Parameters:

  • headers (Hash)

    Hash of header name => value pairs



88
89
90
91
# File 'lib/otto/security/core.rb', line 88

def set_security_headers(headers)
  ensure_not_frozen!
  @security_config.security_headers.merge!(headers)
end