otto 1.1.0.pre.alpha4 → 1.3.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.
@@ -0,0 +1,276 @@
1
+ # examples/security_features/app.rb (Updated with Design System)
2
+
3
+ require_relative '../../lib/otto/design_system'
4
+
5
+ class SecureApp
6
+ include Otto::DesignSystem
7
+ include Otto::Security::CSRFHelpers
8
+
9
+ attr_reader :req, :res
10
+
11
+ def initialize(req, res)
12
+ @req = req
13
+ @res = res
14
+ res.headers['content-type'] = 'text/html; charset=utf-8'
15
+ end
16
+
17
+ def otto
18
+ self.class.otto
19
+ end
20
+
21
+ def index
22
+ csrf_tag = respond_to?(:csrf_form_tag) ? csrf_form_tag : ''
23
+
24
+ content = <<~HTML
25
+ <div class="otto-card otto-text-center">
26
+ <img src="/img/otto.jpg" alt="Otto Framework" class="otto-logo" />
27
+ <h1>Otto Security Features</h1>
28
+ <p class="otto-mb-md">Security demonstration for the Otto framework</p>
29
+ </div>
30
+
31
+ #{otto_card('CSRF Protected Feedback') do
32
+ <<~FORM
33
+ <form method="post" action="/feedback" class="otto-form">
34
+ #{csrf_tag}
35
+ <label>Message:</label>
36
+ #{otto_textarea('message', placeholder: 'Enter your feedback...', required: true)}
37
+ #{otto_button('Submit Feedback', variant: 'primary')}
38
+ </form>
39
+ FORM
40
+ end}
41
+
42
+ #{otto_card('File Upload Validation') do
43
+ <<~UPLOAD
44
+ <form method="post" action="/upload" enctype="multipart/form-data" class="otto-form">
45
+ #{csrf_tag}
46
+ <label>Choose file:</label>
47
+ <input type="file" name="upload_file" class="otto-input">
48
+ #{otto_button('Upload File', variant: 'primary')}
49
+ </form>
50
+ UPLOAD
51
+ end}
52
+
53
+ #{otto_card('User Profile Input Validation') do
54
+ <<~PROFILE
55
+ <form method="post" action="/profile" class="otto-form">
56
+ #{csrf_tag}
57
+ <label>Name:</label>
58
+ #{otto_input('name', placeholder: 'Your name', required: true)}
59
+
60
+ <label>Email:</label>
61
+ #{otto_input('email', type: 'email', placeholder: 'your@email.com', required: true)}
62
+
63
+ <label>Bio:</label>
64
+ #{otto_textarea('bio', placeholder: 'Tell us about yourself...')}
65
+
66
+ #{otto_button('Update Profile', variant: 'primary')}
67
+ </form>
68
+ PROFILE
69
+ end}
70
+
71
+ #{otto_card('Security Information') do
72
+ <<~INFO
73
+ <h3>Security Features Active:</h3>
74
+ <ul>
75
+ <li><strong>CSRF Protection:</strong> All forms include CSRF tokens</li>
76
+ <li><strong>Input Validation:</strong> Server-side validation with length limits</li>
77
+ <li><strong>XSS Prevention:</strong> HTML escaping for all user inputs</li>
78
+ <li><strong>Security Headers:</strong> Comprehensive header security policy</li>
79
+ </ul>
80
+
81
+ <p class="otto-mt-md">
82
+ <strong>Test XSS Protection:</strong> Try entering
83
+ #{otto_code_block('<script>alert("XSS")</script>', 'html')}
84
+ in any form field to see input sanitization in action.
85
+ </p>
86
+
87
+ <p class="otto-mt-md">
88
+ #{otto_link('View Request Headers', '/headers')}
89
+ </p>
90
+ INFO
91
+ end}
92
+ HTML
93
+
94
+ res.body = otto_page(content, 'Otto Security Features')
95
+ end
96
+
97
+ def receive_feedback
98
+ begin
99
+ message = req.params['message']
100
+
101
+ if respond_to?(:validate_input)
102
+ safe_message = validate_input(message, max_length: 1000, allow_html: false)
103
+ else
104
+ safe_message = message.to_s.strip
105
+ raise 'Message too long' if safe_message.length > 1000
106
+ end
107
+
108
+ content = if safe_message.empty?
109
+ otto_alert('error', 'Validation Error', 'Message cannot be empty.')
110
+ else
111
+ <<~HTML
112
+ #{otto_alert('success', 'Feedback Received', 'Thank you for your feedback!')}
113
+
114
+ #{otto_card('Your Message') do
115
+ otto_code_block(safe_message, 'text')
116
+ end}
117
+ HTML
118
+ end
119
+ rescue Otto::Security::ValidationError => ex
120
+ content = otto_alert('error', 'Security Validation Failed', ex.message)
121
+ rescue StandardError
122
+ content = otto_alert('error', 'Processing Error', 'An error occurred processing your request.')
123
+ end
124
+
125
+ content += "<p class=\"otto-mt-lg\">#{otto_link('← Back to form', '/')}</p>"
126
+ res.body = otto_page(content, 'Feedback Response')
127
+ end
128
+
129
+ def upload_file
130
+ begin
131
+ uploaded_file = req.params['upload_file']
132
+
133
+ if uploaded_file.nil? || uploaded_file.empty?
134
+ content = otto_alert('error', 'Upload Error', 'No file was selected.')
135
+ else
136
+ begin
137
+ filename = begin
138
+ uploaded_file[:filename]
139
+ rescue StandardError
140
+ uploaded_file.original_filename
141
+ end
142
+ rescue StandardError
143
+ 'unknown'
144
+ end
145
+
146
+ safe_filename = if respond_to?(:sanitize_filename)
147
+ sanitize_filename(filename)
148
+ else
149
+ File.basename(filename.to_s).gsub(/[^\w\-_\.]/, '_')
150
+ end
151
+
152
+ file_info = {
153
+ 'Original filename' => filename,
154
+ 'Sanitized filename' => safe_filename,
155
+ 'Content type' => uploaded_file[:type] || 'unknown',
156
+ 'Security status' => 'File validated and processed safely',
157
+ }
158
+
159
+ info_html = file_info.map do |key, value|
160
+ "<p><strong>#{key}:</strong> #{escape_html(value)}</p>"
161
+ end.join
162
+
163
+ content = <<~HTML
164
+ #{otto_alert('success', 'File Upload Successful', 'File processed and validated successfully!')}
165
+
166
+ #{otto_card('File Information') do
167
+ info_html
168
+ end}
169
+
170
+ <div class="otto-alert otto-alert-info">
171
+ <h3 class="otto-alert-title">Demo Notice</h3>
172
+ <p class="otto-alert-message">This is a demonstration - files are validated but not permanently stored.</p>
173
+ </div>
174
+ HTML
175
+ end
176
+ rescue Otto::Security::ValidationError => ex
177
+ content = otto_alert('error', 'File Validation Failed', ex.message)
178
+ rescue StandardError
179
+ content = otto_alert('error', 'Upload Error', 'An error occurred during file upload.')
180
+ end
181
+
182
+ content += "<p class=\"otto-mt-lg\">#{otto_link('← Back to form', '/')}</p>"
183
+ res.body = otto_page(content, 'Upload Response')
184
+ end
185
+
186
+ def update_profile
187
+ begin
188
+ name = req.params['name']
189
+ email = req.params['email']
190
+ bio = req.params['bio']
191
+
192
+ if respond_to?(:validate_input)
193
+ safe_name = validate_input(name, max_length: 100)
194
+ safe_email = validate_input(email, max_length: 255)
195
+ safe_bio = validate_input(bio, max_length: 500, allow_html: false)
196
+ else
197
+ safe_name = name.to_s.strip[0..99]
198
+ safe_email = email.to_s.strip[0..254]
199
+ safe_bio = bio.to_s.strip[0..499]
200
+ end
201
+
202
+ unless safe_email.match?(/\A[^@\s]+@[^@\s]+\z/)
203
+ raise Otto::Security::ValidationError, 'Invalid email format'
204
+ end
205
+
206
+ profile_data = {
207
+ 'Name' => safe_name,
208
+ 'Email' => safe_email,
209
+ 'Bio' => safe_bio,
210
+ 'Updated' => Time.now.strftime('%Y-%m-%d %H:%M:%S UTC'),
211
+ }
212
+
213
+ profile_html = profile_data.map do |key, value|
214
+ "<p><strong>#{key}:</strong> #{escape_html(value)}</p>"
215
+ end.join
216
+
217
+ content = <<~HTML
218
+ #{otto_alert('success', 'Profile Updated', 'Your profile has been updated successfully!')}
219
+
220
+ #{otto_card('Profile Data') do
221
+ profile_html
222
+ end}
223
+ HTML
224
+ rescue Otto::Security::ValidationError => ex
225
+ content = otto_alert('error', 'Profile Validation Failed', ex.message)
226
+ rescue StandardError
227
+ content = otto_alert('error', 'Update Error', 'An error occurred updating your profile.')
228
+ end
229
+
230
+ content += "<p class=\"otto-mt-lg\">#{otto_link('← Back to form', '/')}</p>"
231
+ res.body = otto_page(content, 'Profile Update')
232
+ end
233
+
234
+ def show_headers
235
+ res.headers['content-type'] = 'application/json; charset=utf-8'
236
+
237
+ safe_headers = {}
238
+ req.env.each do |key, value|
239
+ if key.start_with?('HTTP_') || %w[REQUEST_METHOD PATH_INFO QUERY_STRING].include?(key)
240
+ safe_headers[key] = value.to_s[0..200]
241
+ end
242
+ end
243
+
244
+ response_data = {
245
+ message: 'Request headers analysis (filtered for security)',
246
+ client_ip: req.respond_to?(:client_ipaddress) ? req.client_ipaddress : req.ip,
247
+ secure_connection: req.respond_to?(:secure?) ? req.secure? : false,
248
+ timestamp: Time.now.utc.iso8601,
249
+ headers: safe_headers,
250
+ security_analysis: {
251
+ csrf_protection: respond_to?(:csrf_token_valid?) ? 'Active' : 'Basic',
252
+ content_security: 'Headers validated and filtered',
253
+ xss_protection: 'HTML escaping enabled',
254
+ },
255
+ }
256
+
257
+ require 'json'
258
+ res.body = JSON.pretty_generate(response_data)
259
+ end
260
+
261
+ def not_found
262
+ res.status = 404
263
+ content = otto_alert('error', 'Page Not Found', 'The requested page could not be found.')
264
+ content += "<p>#{otto_link('← Back to home', '/')}</p>"
265
+ res.body = otto_page(content, '404 - Not Found')
266
+ end
267
+
268
+ def server_error
269
+ res.status = 500
270
+ error_id = req.env['otto.error_id'] || SecureRandom.hex(8)
271
+ content = otto_alert('error', 'Server Error', 'An internal server error occurred.')
272
+ content += "<p><small>Error ID: #{escape_html(error_id)}</small></p>"
273
+ content += "<p>#{otto_link('← Back to home', '/')}</p>"
274
+ res.body = otto_page(content, '500 - Server Error')
275
+ end
276
+ end
@@ -0,0 +1,83 @@
1
+ # examples/security_features/config.ru
2
+
3
+ # OTTO SECURE EXAMPLE APP CONFIG - 2025-07-18
4
+ #
5
+ # Usage:
6
+ #
7
+ # $ thin -e dev -R config.ru -p 10770 start
8
+ #
9
+
10
+ public_path = File.expand_path('../../public', __dir__)
11
+
12
+ require_relative '../../lib/otto'
13
+ require_relative 'app'
14
+
15
+ # Create Otto app with security features enabled
16
+ app = Otto.new('./routes', {
17
+ # Enable CSRF protection for POST, PUT, DELETE requests
18
+ csrf_protection: true,
19
+
20
+ # Enable input validation and sanitization
21
+ request_validation: true,
22
+
23
+ # Configure trusted proxy servers (adjust for your infrastructure)
24
+ trusted_proxies: [
25
+ # The primary RFC 1918 private ranges
26
+ '127.0.0.1', # Local development
27
+ '10.0.0.0/8', # Private Class A
28
+ '172.16.0.0/12', # Private Class B
29
+ '192.168.0.0/16', # Private Class C
30
+
31
+ # Other reserved ranges that I often forget about
32
+ # '127.0.0.0/8', # Loopback
33
+ # '100.64.0.0/10', # Carrier-grade NAT
34
+ # '169.254.0.0/16', # RFC 3927 - Automatic Private IP Addressing (APIPA)
35
+ # '198.18.0.0/15', # RFC 2544 - Benchmarking methodology
36
+ # '203.0.113.0/24', # RFC 5737 - Documentation examples
37
+ # '224.0.0.0/4', # Multicast
38
+ # '240.0.0.0/4', # RFC 1112 - Class E (experimental)
39
+ ],
40
+
41
+ # Custom security headers
42
+ security_headers: {
43
+ 'content-security-policy' => "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'",
44
+ 'strict-transport-security' => 'max-age=31536000; includeSubDomains',
45
+ 'x-frame-options' => 'DENY',
46
+ },
47
+ }
48
+ )
49
+
50
+ # Optional: Configure additional security settings
51
+ app.security_config.max_request_size = 5 * 1024 * 1024 # 5MB limit
52
+ app.security_config.max_param_depth = 10 # Limit parameter nesting
53
+ app.security_config.max_param_keys = 50 # Limit parameters per request
54
+
55
+ # Optional: Add static file serving with security
56
+ app.option[:public] = public_path
57
+
58
+ # Development vs Production configuration
59
+ if ENV['RACK_ENV'] == 'production'
60
+ # Production-specific settings
61
+ app.security_config.require_secure_cookies = true
62
+
63
+ # More restrictive CSP for production
64
+ app.set_security_headers({
65
+ 'content-security-policy' => "default-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'",
66
+ 'strict-transport-security' => 'max-age=63072000; includeSubDomains; preload',
67
+ },
68
+ )
69
+ else
70
+ # Development-specific settings
71
+ puts '🔒 Security features enabled:'
72
+ puts ' ✓ CSRF Protection'
73
+ puts ' ✓ Input Validation'
74
+ puts ' ✓ Request Size Limits'
75
+ puts ' ✓ Security Headers'
76
+ puts ' ✓ Trusted Proxy Support'
77
+ puts ''
78
+ end
79
+
80
+ # Mount the application
81
+ map('/') do
82
+ run app
83
+ end
@@ -0,0 +1,11 @@
1
+ # examples/security_features/routes
2
+
3
+ GET / SecureApp#index
4
+ POST /feedback SecureApp#receive_feedback
5
+ POST /upload SecureApp#upload_file
6
+ POST /profile SecureApp#update_profile
7
+ GET /headers SecureApp#show_headers
8
+
9
+ # Error handlers (optional)
10
+ GET /404 SecureApp#not_found
11
+ GET /500 SecureApp#server_error