otto 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/otto/route.rb CHANGED
@@ -33,20 +33,20 @@ class Otto
33
33
  # @param definition [String] Class and method definition (Class.method or Class#method)
34
34
  # @raise [ArgumentError] if definition format is invalid or class name is unsafe
35
35
  def initialize(verb, path, definition)
36
- @verb = verb.to_s.upcase.to_sym
37
- @path = path
38
- @definition = definition
36
+ @verb = verb.to_s.upcase.to_sym
37
+ @path = path
38
+ @definition = definition
39
39
  @pattern, @keys = *compile(@path)
40
40
  if !@definition.index('.').nil?
41
41
  @klass, @name = @definition.split('.')
42
- @kind = :class
42
+ @kind = :class
43
43
  elsif !@definition.index('#').nil?
44
44
  @klass, @name = @definition.split('#')
45
- @kind = :instance
45
+ @kind = :instance
46
46
  else
47
47
  raise ArgumentError, "Bad definition: #{@definition}"
48
48
  end
49
- @klass = safe_const_get(@klass)
49
+ @klass = safe_const_get(@klass)
50
50
  # @method = @klass.method(@name)
51
51
  end
52
52
 
@@ -85,8 +85,8 @@ class Otto
85
85
 
86
86
  begin
87
87
  Object.const_get(class_name)
88
- rescue NameError => e
89
- raise ArgumentError, "Class not found: #{class_name} - #{e.message}"
88
+ rescue NameError => ex
89
+ raise ArgumentError, "Class not found: #{class_name} - #{ex.message}"
90
90
  end
91
91
  end
92
92
 
@@ -109,11 +109,16 @@ class Otto
109
109
  # @return [Array] Rack response array [status, headers, body]
110
110
  def call(env, extra_params = {})
111
111
  extra_params ||= {}
112
- req = Rack::Request.new(env)
113
- res = Rack::Response.new
112
+ req = Rack::Request.new(env)
113
+ res = Rack::Response.new
114
114
  req.extend Otto::RequestHelpers
115
115
  res.extend Otto::ResponseHelpers
116
- res.request = req
116
+ res.request = req
117
+
118
+ # Make security config available to response helpers
119
+ if otto.respond_to?(:security_config) && otto.security_config
120
+ env['otto.security_config'] = otto.security_config
121
+ end
117
122
 
118
123
  # Process parameters through security layer
119
124
  req.params.merge! extra_params
@@ -158,7 +163,7 @@ class Otto
158
163
  keys = []
159
164
  if path.respond_to? :to_str
160
165
  special_chars = %w[. + ( ) $]
161
- pattern =
166
+ pattern =
162
167
  path.to_str.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match|
163
168
  case match
164
169
  when '*'
@@ -22,26 +22,29 @@ class Otto
22
22
  # config.max_param_depth = 16
23
23
  class Config
24
24
  attr_accessor :csrf_protection, :csrf_token_key, :csrf_header_key, :csrf_session_key,
25
- :max_request_size, :max_param_depth, :max_param_keys,
26
- :trusted_proxies, :require_secure_cookies,
27
- :security_headers, :input_validation
25
+ :max_request_size, :max_param_depth, :max_param_keys,
26
+ :trusted_proxies, :require_secure_cookies,
27
+ :security_headers, :input_validation,
28
+ :csp_nonce_enabled, :debug_csp
28
29
 
29
30
  # Initialize security configuration with safe defaults
30
31
  #
31
32
  # All security features are disabled by default to maintain backward
32
33
  # compatibility with existing Otto applications.
33
34
  def initialize
34
- @csrf_protection = false
35
- @csrf_token_key = '_csrf_token'
36
- @csrf_header_key = 'HTTP_X_CSRF_TOKEN'
37
- @csrf_session_key = '_csrf_session_id'
38
- @max_request_size = 10 * 1024 * 1024 # 10MB
39
- @max_param_depth = 32
40
- @max_param_keys = 64
41
- @trusted_proxies = []
35
+ @csrf_protection = false
36
+ @csrf_token_key = '_csrf_token'
37
+ @csrf_header_key = 'HTTP_X_CSRF_TOKEN'
38
+ @csrf_session_key = '_csrf_session_id'
39
+ @max_request_size = 10 * 1024 * 1024 # 10MB
40
+ @max_param_depth = 32
41
+ @max_param_keys = 64
42
+ @trusted_proxies = []
42
43
  @require_secure_cookies = false
43
- @security_headers = default_security_headers
44
- @input_validation = true
44
+ @security_headers = default_security_headers
45
+ @input_validation = true
46
+ @csp_nonce_enabled = false
47
+ @debug_csp = false
45
48
  end
46
49
 
47
50
  # Enable CSRF (Cross-Site Request Forgery) protection
@@ -96,7 +99,7 @@ class Otto
96
99
  when Array
97
100
  @trusted_proxies.concat(proxy)
98
101
  else
99
- raise ArgumentError, "Proxy must be a String or Array"
102
+ raise ArgumentError, 'Proxy must be a String or Array'
100
103
  end
101
104
  end
102
105
 
@@ -130,19 +133,19 @@ class Otto
130
133
  size = content_length.to_i
131
134
  if size > @max_request_size
132
135
  raise Otto::Security::RequestTooLargeError,
133
- "Request size #{size} exceeds maximum #{@max_request_size}"
136
+ "Request size #{size} exceeds maximum #{@max_request_size}"
134
137
  end
135
138
  true
136
139
  end
137
140
 
138
141
  def generate_csrf_token(session_id = nil)
139
- base = session_id || 'no-session'
140
- token = SecureRandom.hex(32)
142
+ base = session_id || 'no-session'
143
+ token = SecureRandom.hex(32)
141
144
  hash_input = base + ':' + token
142
- signature = Digest::SHA256.hexdigest(hash_input)
145
+ signature = Digest::SHA256.hexdigest(hash_input)
143
146
  csrf_token = "#{token}:#{signature}"
144
147
 
145
- puts "=== CSRF Generation ==="
148
+ puts '=== CSRF Generation ==='
146
149
  puts "hash_input: #{hash_input.inspect}"
147
150
  puts "signature: #{signature}"
148
151
  puts "csrf_token: #{csrf_token}"
@@ -156,12 +159,12 @@ class Otto
156
159
  token_part, signature = token.split(':')
157
160
  return false if token_part.nil? || signature.nil?
158
161
 
159
- base = session_id || 'no-session'
160
- hash_input = "#{base}:#{token_part}"
162
+ base = session_id || 'no-session'
163
+ hash_input = "#{base}:#{token_part}"
161
164
  expected_signature = Digest::SHA256.hexdigest(hash_input)
162
- comparison_result = secure_compare(signature, expected_signature)
165
+ comparison_result = secure_compare(signature, expected_signature)
163
166
 
164
- puts "=== CSRF Verification ==="
167
+ puts '=== CSRF Verification ==='
165
168
  puts "hash_input: #{hash_input.inspect}"
166
169
  puts "received_signature: #{signature}"
167
170
  puts "expected_signature: #{expected_signature}"
@@ -179,9 +182,9 @@ class Otto
179
182
  # @param max_age [Integer] Maximum age in seconds (default: 1 year)
180
183
  # @param include_subdomains [Boolean] Apply to all subdomains (default: true)
181
184
  # @return [void]
182
- def enable_hsts!(max_age: 31536000, include_subdomains: true)
183
- hsts_value = "max-age=#{max_age}"
184
- hsts_value += "; includeSubDomains" if include_subdomains
185
+ def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
186
+ hsts_value = "max-age=#{max_age}"
187
+ hsts_value += '; includeSubDomains' if include_subdomains
185
188
  @security_headers['strict-transport-security'] = hsts_value
186
189
  end
187
190
 
@@ -199,6 +202,53 @@ class Otto
199
202
  @security_headers['content-security-policy'] = policy
200
203
  end
201
204
 
205
+ # Enable Content Security Policy (CSP) with nonce support
206
+ #
207
+ # This enables dynamic CSP header generation with nonces for enhanced security.
208
+ # Unlike enable_csp!, this doesn't set a static policy but enables the response
209
+ # helper to generate CSP headers with nonces on a per-request basis.
210
+ #
211
+ # @param debug [Boolean] Enable debug logging for CSP headers (default: false)
212
+ # @return [void]
213
+ #
214
+ # @example
215
+ # config.enable_csp_with_nonce!(debug: true)
216
+ def enable_csp_with_nonce!(debug: false)
217
+ @csp_nonce_enabled = true
218
+ @debug_csp = debug
219
+ end
220
+
221
+ # Disable CSP nonce support
222
+ #
223
+ # @return [void]
224
+ def disable_csp_nonce!
225
+ @csp_nonce_enabled = false
226
+ end
227
+
228
+ # Check if CSP nonce support is enabled
229
+ #
230
+ # @return [Boolean] true if CSP nonce support is enabled
231
+ def csp_nonce_enabled?
232
+ @csp_nonce_enabled
233
+ end
234
+
235
+ # Check if CSP debug logging is enabled
236
+ #
237
+ # @return [Boolean] true if CSP debug logging is enabled
238
+ def debug_csp?
239
+ @debug_csp
240
+ end
241
+
242
+ # Generate a CSP policy string with the provided nonce
243
+ #
244
+ # @param nonce [String] The nonce value to include in the CSP
245
+ # @param development_mode [Boolean] Whether to use development-friendly directives
246
+ # @return [String] Complete CSP policy string
247
+ def generate_nonce_csp(nonce, development_mode: false)
248
+ directives = development_mode ? development_csp_directives(nonce) : production_csp_directives(nonce)
249
+ directives.join(' ')
250
+ end
251
+
202
252
  # Enable X-Frame-Options header to prevent clickjacking
203
253
  #
204
254
  # @param option [String] Frame options: 'DENY', 'SAMEORIGIN', or 'ALLOW-FROM uri'
@@ -245,27 +295,23 @@ class Otto
245
295
  return session[csrf_session_key] if session[csrf_session_key]
246
296
  return session['session_id'] if session['session_id']
247
297
  end
248
- rescue
298
+ rescue StandardError
249
299
  # Fall through to cookies
250
300
  end
251
301
 
252
302
  # Try cookies
253
303
  request.cookies['_otto_session'] ||
254
- request.cookies['session_id'] ||
255
- request.cookies['_session_id']
304
+ request.cookies['session_id'] ||
305
+ request.cookies['_session_id']
256
306
  end
257
307
 
258
308
  def store_session_id(request, session_id)
259
- begin
260
- session = request.session
309
+ session = request.session
261
310
  session[csrf_session_key] = session_id if session
262
- rescue
263
- # Cookie fallback handled in inject_csrf_token
264
- end
311
+ rescue StandardError
312
+ # Cookie fallback handled in inject_csrf_token
265
313
  end
266
314
 
267
- private
268
-
269
315
  # Default security headers applied to all responses
270
316
  #
271
317
  # These headers provide basic defense against common web vulnerabilities:
@@ -282,7 +328,7 @@ class Otto
282
328
  {
283
329
  'x-content-type-options' => 'nosniff',
284
330
  'x-xss-protection' => '1; mode=block',
285
- 'referrer-policy' => 'strict-origin-when-cross-origin'
331
+ 'referrer-policy' => 'strict-origin-when-cross-origin',
286
332
  }
287
333
  end
288
334
 
@@ -298,10 +344,58 @@ class Otto
298
344
  def secure_compare(a, b)
299
345
  return false if a.nil? || b.nil? || a.length != b.length
300
346
 
301
- result = 0
347
+ result = 0
302
348
  a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
303
349
  result == 0
304
350
  end
351
+
352
+ # Generate CSP directives for development environment
353
+ #
354
+ # Development mode allows inline scripts/styles and hot reloading connections
355
+ # for better developer experience with build tools like Vite.
356
+ #
357
+ # @param nonce [String] The nonce value to include in script-src
358
+ # @return [Array<String>] Array of CSP directive strings
359
+ def development_csp_directives(nonce)
360
+ [
361
+ "default-src 'none';",
362
+ "script-src 'nonce-#{nonce}' 'unsafe-inline';", # Allow inline scripts for development tools
363
+ "style-src 'self' 'unsafe-inline';",
364
+ "connect-src 'self' ws: wss: http: https:;", # Allow HTTP and all WebSocket connections for dev tools
365
+ "img-src 'self' data:;",
366
+ "font-src 'self';",
367
+ "object-src 'none';",
368
+ "base-uri 'self';",
369
+ "form-action 'self';",
370
+ "frame-ancestors 'none';",
371
+ "manifest-src 'self';",
372
+ "worker-src 'self' data:;",
373
+ ]
374
+ end
375
+
376
+ # Generate CSP directives for production environment
377
+ #
378
+ # Production mode is more restrictive, only allowing HTTPS connections
379
+ # and nonce-only scripts for enhanced XSS protection.
380
+ #
381
+ # @param nonce [String] The nonce value to include in script-src
382
+ # @return [Array<String>] Array of CSP directive strings
383
+ def production_csp_directives(nonce)
384
+ [
385
+ "default-src 'none';", # Restrict to same origin by default
386
+ "script-src 'nonce-#{nonce}';", # Only allow scripts with valid nonce
387
+ "style-src 'self' 'unsafe-inline';", # Allow inline styles and same-origin stylesheets
388
+ "connect-src 'self' wss: https:;", # Only HTTPS and secure WebSockets
389
+ "img-src 'self' data:;", # Allow images from same origin and data URIs
390
+ "font-src 'self';", # Allow fonts from same origin only
391
+ "object-src 'none';", # Block <object>, <embed>, and <applet> elements
392
+ "base-uri 'self';", # Restrict <base> tag targets to same origin
393
+ "form-action 'self';", # Restrict form submissions to same origin
394
+ "frame-ancestors 'none';", # Prevent site from being embedded in frames
395
+ "manifest-src 'self';", # Allow web app manifests from same origin
396
+ "worker-src 'self' data:;", # Allow Workers from same origin and data blobs
397
+ ]
398
+ end
305
399
  end
306
400
 
307
401
  # Raised when a request exceeds the configured size limit
@@ -3,12 +3,14 @@
3
3
  require 'securerandom'
4
4
 
5
5
  class Otto
6
+ # CSRF protection middleware for Otto framework
6
7
  module Security
8
+ # Middleware that provides Cross-Site Request Forgery (CSRF) protection
7
9
  class CSRFMiddleware
8
10
  SAFE_METHODS = %w[GET HEAD OPTIONS TRACE].freeze
9
11
 
10
12
  def initialize(app, config = nil)
11
- @app = app
13
+ @app = app
12
14
  @config = config || Otto::Security::Config.new
13
15
  end
14
16
 
@@ -25,9 +27,7 @@ class Otto
25
27
  end
26
28
 
27
29
  # Validate CSRF token for unsafe methods
28
- unless valid_csrf_token?(request)
29
- return csrf_error_response
30
- end
30
+ return csrf_error_response unless valid_csrf_token?(request)
31
31
 
32
32
  @app.call(env)
33
33
  end
@@ -54,8 +54,7 @@ class Otto
54
54
  token ||= request.env[@config.csrf_header_key]
55
55
 
56
56
  # Try alternative header format
57
- token ||= request.env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' ?
58
- request.env['HTTP_X_CSRF_TOKEN'] : nil
57
+ token ||= request.env['HTTP_X_CSRF_TOKEN'] if request.env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
59
58
 
60
59
  token
61
60
  end
@@ -68,7 +67,7 @@ class Otto
68
67
  return response unless response.is_a?(Array) && response.length >= 3
69
68
 
70
69
  status, headers, body = response
71
- content_type = headers.find { |k, v| k.downcase == 'content-type' }&.last
70
+ content_type = headers.find { |k, _v| k.downcase == 'content-type' }&.last
72
71
 
73
72
  return response unless content_type&.include?('text/html')
74
73
 
@@ -85,14 +84,12 @@ class Otto
85
84
  body_content = body.respond_to?(:join) ? body.join : body.to_s
86
85
 
87
86
  if body_content.match?(/<head>/i)
88
- meta_tag = %(<meta name="csrf-token" content="#{csrf_token}">)
87
+ meta_tag = %(<meta name="csrf-token" content="#{csrf_token}">)
89
88
  body_content = body_content.sub(/<head>/i, "<head>\n#{meta_tag}")
90
89
 
91
90
  # Update content length if present
92
- content_length_key = headers.keys.find { |k| k.downcase == 'content-length' }
93
- if content_length_key
94
- headers[content_length_key] = body_content.bytesize.to_s
95
- end
91
+ content_length_key = headers.keys.find { |k| k.downcase == 'content-length' }
92
+ headers[content_length_key] = body_content.bytesize.to_s if content_length_key
96
93
 
97
94
  [status, headers, [body_content]]
98
95
  else
@@ -106,8 +103,8 @@ class Otto
106
103
  return if existing_cookie == session_id
107
104
 
108
105
  # Set the session cookie
109
- cookie_value = "#{session_id}; Path=/; HttpOnly; SameSite=Lax"
110
- cookie_value += "; Secure" if request.scheme == 'https'
106
+ cookie_value = "#{session_id}; Path=/; HttpOnly; SameSite=Lax"
107
+ cookie_value += '; Secure' if request.scheme == 'https'
111
108
 
112
109
  # Handle existing Set-Cookie headers
113
110
  existing_cookies = headers['set-cookie'] || headers['Set-Cookie']
@@ -126,8 +123,8 @@ class Otto
126
123
  def html_response?(response)
127
124
  return false unless response.is_a?(Array) && response.length >= 2
128
125
 
129
- headers = response[1]
130
- content_type = headers.find { |k, v| k.downcase == 'content-type' }&.last
126
+ headers = response[1]
127
+ content_type = headers.find { |k, _v| k.downcase == 'content-type' }&.last
131
128
  content_type&.include?('text/html')
132
129
  end
133
130
 
@@ -136,34 +133,30 @@ class Otto
136
133
  403,
137
134
  {
138
135
  'content-type' => 'application/json',
139
- 'content-length' => csrf_error_body.bytesize.to_s
136
+ 'content-length' => csrf_error_body.bytesize.to_s,
140
137
  },
141
- [csrf_error_body]
138
+ [csrf_error_body],
142
139
  ]
143
140
  end
144
141
 
145
142
  def csrf_error_body
146
143
  {
147
144
  error: 'CSRF token validation failed',
148
- message: 'The request could not be authenticated. Please refresh the page and try again.'
145
+ message: 'The request could not be authenticated. Please refresh the page and try again.',
149
146
  }.to_json
150
147
  end
151
-
152
148
  end
153
149
 
150
+ # Helper methods for CSRF token handling in views and controllers
154
151
  module CSRFHelpers
155
152
  def csrf_token
156
153
  if @csrf_token.nil? && otto.respond_to?(:security_config)
157
- session_id = otto.security_config.get_or_create_session_id(req)
154
+ session_id = otto.security_config.get_or_create_session_id(req)
158
155
  @csrf_token = otto.security_config.generate_csrf_token(session_id)
159
156
  end
160
157
  @csrf_token
161
158
  end
162
159
 
163
- private
164
-
165
- public
166
-
167
160
  def csrf_meta_tag
168
161
  %(<meta name="csrf-token" content="#{csrf_token}">)
169
162
  end
@@ -173,8 +166,11 @@ class Otto
173
166
  end
174
167
 
175
168
  def csrf_token_key
176
- otto.respond_to?(:security_config) ?
177
- otto.security_config.csrf_token_key : '_csrf_token'
169
+ if otto.respond_to?(:security_config)
170
+ otto.security_config.csrf_token_key
171
+ else
172
+ '_csrf_token'
173
+ end
178
174
  end
179
175
  end
180
176
  end
@@ -5,31 +5,32 @@ require 'cgi'
5
5
 
6
6
  class Otto
7
7
  module Security
8
+ # ValidationMiddleware provides input validation and sanitization for web requests
8
9
  class ValidationMiddleware
9
10
  # Character validation patterns
10
- INVALID_CHARACTERS = /[\x00-\x1f\x7f-\xff]/n.freeze
11
- NULL_BYTE = /\0/.freeze
11
+ INVALID_CHARACTERS = /[\x00-\x1f\x7f-\xff]/n
12
+ NULL_BYTE = /\0/
12
13
 
13
14
  DANGEROUS_PATTERNS = [
14
15
  /<script[^>]*>/i, # Script tags
15
16
  /javascript:/i, # JavaScript protocol
16
17
  /data:.*base64/i, # Data URLs with base64
17
- /on\w+\s*=/i, # Event handlers
18
+ /on\w+\s*=/i, # Event handlers
18
19
  /expression\s*\(/i, # CSS expressions
19
- /url\s*\(/i, # CSS url() functions
20
- NULL_BYTE, # Null bytes
21
- INVALID_CHARACTERS # Control characters and extended ASCII
20
+ /url\s*\(/i, # CSS url() functions
21
+ NULL_BYTE, # Null bytes
22
+ INVALID_CHARACTERS, # Control characters and extended ASCII
22
23
  ].freeze
23
24
 
24
25
  SQL_INJECTION_PATTERNS = [
25
26
  /('|(\\')|(;)|(\\)|(--)|(%27)|(%3B)|(%3D))/i,
26
27
  /(union|select|insert|update|delete|drop|create|alter|exec|execute)/i,
27
28
  /(or|and)\s+\w+\s*=\s*\w+/i,
28
- /\d+\s*(=|>|<|>=|<=|<>|!=)\s*\d+/i
29
+ /\d+\s*(=|>|<|>=|<=|<>|!=)\s*\d+/i,
29
30
  ].freeze
30
31
 
31
32
  def initialize(app, config = nil)
32
- @app = app
33
+ @app = app
33
34
  @config = config || Otto::Security::Config.new
34
35
  end
35
36
 
@@ -46,17 +47,21 @@ class Otto
46
47
  validate_content_type(request)
47
48
 
48
49
  # Validate and sanitize parameters
49
- validate_parameters(request) if request.params
50
+ begin
51
+ validate_parameters(request) if request.params
52
+ rescue Rack::QueryParser::QueryLimitError => ex
53
+ # Handle Rack's built-in query parsing limits
54
+ raise Otto::Security::ValidationError, "Parameter structure too complex: #{ex.message}"
55
+ end
50
56
 
51
57
  # Validate headers
52
58
  validate_headers(request)
53
59
 
54
60
  @app.call(env)
55
-
56
- rescue Otto::Security::ValidationError => e
57
- return validation_error_response(e.message)
58
- rescue Otto::Security::RequestTooLargeError => e
59
- return request_too_large_response(e.message)
61
+ rescue Otto::Security::ValidationError => ex
62
+ validation_error_response(ex.message)
63
+ rescue Otto::Security::RequestTooLargeError => ex
64
+ request_too_large_response(ex.message)
60
65
  end
61
66
  end
62
67
 
@@ -76,7 +81,7 @@ class Otto
76
81
  'application/x-shockwave-flash',
77
82
  'application/x-silverlight-app',
78
83
  'text/vbscript',
79
- 'application/vbscript'
84
+ 'application/vbscript',
80
85
  ]
81
86
 
82
87
  if dangerous_types.any? { |type| content_type.downcase.include?(type) }
@@ -148,14 +153,14 @@ class Otto
148
153
  # Check for dangerous patterns
149
154
  DANGEROUS_PATTERNS.each do |pattern|
150
155
  if value.match?(pattern)
151
- raise Otto::Security::ValidationError, "Dangerous content detected in parameter"
156
+ raise Otto::Security::ValidationError, 'Dangerous content detected in parameter'
152
157
  end
153
158
  end
154
159
 
155
160
  # Check for SQL injection patterns
156
161
  SQL_INJECTION_PATTERNS.each do |pattern|
157
162
  if value.match?(pattern)
158
- raise Otto::Security::ValidationError, "Potential SQL injection detected"
163
+ raise Otto::Security::ValidationError, 'Potential SQL injection detected'
159
164
  end
160
165
  end
161
166
 
@@ -169,9 +174,7 @@ class Otto
169
174
 
170
175
  # Additional sanitization for common attack vectors
171
176
  sanitized = sanitized.gsub(/<!--.*?-->/m, '') # Remove HTML comments
172
- sanitized = sanitized.gsub(/<!\[CDATA\[.*?\]\]>/m, '') # Remove CDATA sections
173
-
174
- sanitized
177
+ sanitized.gsub(/<!\[CDATA\[.*?\]\]>/m, '') # Remove CDATA sections
175
178
  end
176
179
 
177
180
  def validate_headers(request)
@@ -197,13 +200,13 @@ class Otto
197
200
  # Validate User-Agent length
198
201
  user_agent = request.env['HTTP_USER_AGENT']
199
202
  if user_agent && user_agent.length > 1000
200
- raise Otto::Security::ValidationError, "User-Agent header too long"
203
+ raise Otto::Security::ValidationError, 'User-Agent header too long'
201
204
  end
202
205
 
203
206
  # Validate Referer header
204
207
  referer = request.env['HTTP_REFERER']
205
208
  if referer && referer.length > 2000
206
- raise Otto::Security::ValidationError, "Referer header too long"
209
+ raise Otto::Security::ValidationError, 'Referer header too long'
207
210
  end
208
211
  end
209
212
 
@@ -212,9 +215,9 @@ class Otto
212
215
  400,
213
216
  {
214
217
  'content-type' => 'application/json',
215
- 'content-length' => validation_error_body(message).bytesize.to_s
218
+ 'content-length' => validation_error_body(message).bytesize.to_s,
216
219
  },
217
- [validation_error_body(message)]
220
+ [validation_error_body(message)],
218
221
  ]
219
222
  end
220
223
 
@@ -223,9 +226,9 @@ class Otto
223
226
  413,
224
227
  {
225
228
  'content-type' => 'application/json',
226
- 'content-length' => request_too_large_body(message).bytesize.to_s
229
+ 'content-length' => request_too_large_body(message).bytesize.to_s,
227
230
  },
228
- [request_too_large_body(message)]
231
+ [request_too_large_body(message)],
229
232
  ]
230
233
  end
231
234
 
@@ -233,7 +236,7 @@ class Otto
233
236
  require 'json'
234
237
  {
235
238
  error: 'Validation failed',
236
- message: message
239
+ message: message,
237
240
  }.to_json
238
241
  end
239
242
 
@@ -241,7 +244,7 @@ class Otto
241
244
  require 'json'
242
245
  {
243
246
  error: 'Request too large',
244
- message: message
247
+ message: message,
245
248
  }.to_json
246
249
  end
247
250
  end
@@ -261,7 +264,7 @@ class Otto
261
264
  unless allow_html
262
265
  ValidationMiddleware::DANGEROUS_PATTERNS.each do |pattern|
263
266
  if input_str.match?(pattern)
264
- raise Otto::Security::ValidationError, "Dangerous content detected"
267
+ raise Otto::Security::ValidationError, 'Dangerous content detected'
265
268
  end
266
269
  end
267
270
  end
@@ -269,7 +272,7 @@ class Otto
269
272
  # Always check for SQL injection
270
273
  ValidationMiddleware::SQL_INJECTION_PATTERNS.each do |pattern|
271
274
  if input_str.match?(pattern)
272
- raise Otto::Security::ValidationError, "Potential SQL injection detected"
275
+ raise Otto::Security::ValidationError, 'Potential SQL injection detected'
273
276
  end
274
277
  end
275
278
 
data/lib/otto/static.rb CHANGED
@@ -17,7 +17,7 @@ class Otto
17
17
  'x-frame-options' => 'DENY',
18
18
  'x-content-type-options' => 'nosniff',
19
19
  'x-xss-protection' => '1; mode=block',
20
- 'referrer-policy' => 'strict-origin-when-cross-origin'
20
+ 'referrer-policy' => 'strict-origin-when-cross-origin',
21
21
  }
22
22
  end
23
23