Class: Rhales::CSP

Inherits:
Object
  • Object
show all
Includes:
Utils::LoggingHelpers
Defined in:
lib/rhales/security/csp.rb

Overview

Content Security Policy (CSP) header generation and management

Provides secure defaults and nonce integration for CSP headers. Converts policy configuration into proper CSP header strings.

Usage: csp = Rhales::CSP.new(config, nonce: ‘abc123’) header = csp.build_header # => “default-src ‘self’; script-src ‘self’ ‘nonce-abc123’; …”

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils::LoggingHelpers

#format_value, #log_timed_operation, #log_with_metadata

Methods included from Utils

#now, #now_in_μs, #pretty_path

Constructor Details

#initialize(config, nonce: nil) ⇒ CSP

Returns a new instance of CSP.



20
21
22
23
# File 'lib/rhales/security/csp.rb', line 20

def initialize(config, nonce: nil)
  @config = config
  @nonce = nonce
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



18
19
20
# File 'lib/rhales/security/csp.rb', line 18

def config
  @config
end

#nonceObject (readonly)

Returns the value of attribute nonce.



18
19
20
# File 'lib/rhales/security/csp.rb', line 18

def nonce
  @nonce
end

Class Method Details

.generate_nonceObject

Generate a new nonce value



62
63
64
65
66
67
68
69
70
# File 'lib/rhales/security/csp.rb', line 62

def self.generate_nonce
  nonce = SecureRandom.hex(16)

  # Log nonce generation for security audit trail. Never log the nonce value
  # itself — it is a per-response secret (issue #57).
  Rhales.logger.debug("CSP nonce generated: length=#{nonce.length} entropy_bits=#{nonce.length * 4}")

  nonce
end

Instance Method Details

#build_headerObject

Build CSP header string from configuration



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/rhales/security/csp.rb', line 26

def build_header
  return nil unless @config.csp_enabled

  policy_directives = []
  nonce_used = false

  @config.csp_policy.each do |directive, sources|
    if sources.empty?
      # For directives with no sources (like upgrade-insecure-requests)
      policy_directives << directive
    else
      # Process sources and interpolate nonce if present
      processed_sources = sources.map do |source|
        interpolated = interpolate_nonce(source)
        nonce_used = true if interpolated != source
        interpolated
      end
      directive_string = "#{directive} #{processed_sources.join(' ')}"
      policy_directives << directive_string
    end
  end

  header = policy_directives.join('; ')

  # Log CSP header generation for security audit. The nonce is a per-response
  # secret, so log only whether one was used, never its value (issue #57).
  (Rhales.logger, :debug, "CSP header generated",
    nonce_used: nonce_used,
    directive_count: policy_directives.size,
    header_length: header.length
  )

  header
end

#interpolate_nonce(source) ⇒ Object (private)

Interpolate nonce placeholder in source values



113
114
115
116
117
# File 'lib/rhales/security/csp.rb', line 113

def interpolate_nonce(source)
  return source unless @nonce && source.include?('{{nonce}}')

  source.gsub('{{nonce}}', @nonce)
end

#nonce_required?Boolean

Check if nonce is required for any directive

Returns:



104
105
106
107
108
# File 'lib/rhales/security/csp.rb', line 104

def nonce_required?
  return false unless @config.csp_enabled

  @config.csp_policy.values.flatten.any? { |source| source.include?('{{nonce}}') }
end

#validate_policy!Object

Validate CSP policy configuration

Raises:



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/rhales/security/csp.rb', line 73

def validate_policy!
  return unless @config.csp_enabled

  errors = []

  # Ensure policy is a hash
  unless @config.csp_policy.is_a?(Hash)
    errors << 'csp_policy must be a hash'
    raise Rhales::Configuration::ConfigurationError, "CSP policy errors: #{errors.join(', ')}"
  end

  # Validate each directive
  @config.csp_policy.each do |directive, sources|
    unless sources.is_a?(Array)
      errors << "#{directive} sources must be an array"
    end

    # Check for dangerous sources
    if sources.include?("'unsafe-eval'")
      errors << "#{directive} contains dangerous 'unsafe-eval' source"
    end

    if sources.include?("'unsafe-inline'") && !%w[style-src].include?(directive)
      errors << "#{directive} contains dangerous 'unsafe-inline' source"
    end
  end

  raise Rhales::Configuration::ConfigurationError, "CSP policy errors: #{errors.join(', ')}" unless errors.empty?
end