otto 1.2.0 → 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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +364 -20
- data/Gemfile +1 -1
- data/Gemfile.lock +76 -46
- data/examples/basic/app.rb +20 -20
- data/examples/basic/config.ru +1 -1
- data/examples/dynamic_pages/app.rb +30 -30
- data/examples/dynamic_pages/config.ru +1 -1
- data/examples/security_features/app.rb +90 -87
- data/examples/security_features/config.ru +19 -17
- data/lib/otto/design_system.rb +22 -22
- data/lib/otto/helpers/request.rb +3 -5
- data/lib/otto/helpers/response.rb +10 -38
- data/lib/otto/route.rb +12 -12
- data/lib/otto/security/config.rb +34 -38
- data/lib/otto/security/csrf.rb +23 -27
- data/lib/otto/security/validator.rb +33 -30
- data/lib/otto/static.rb +1 -1
- data/lib/otto/version.rb +1 -25
- data/lib/otto.rb +97 -60
- data/otto.gemspec +9 -10
- metadata +1 -23
- data/.rubocop_todo.yml +0 -152
- data/VERSION.yml +0 -5
data/examples/basic/app.rb
CHANGED
@@ -8,8 +8,8 @@ class App
|
|
8
8
|
attr_reader :req, :res
|
9
9
|
|
10
10
|
def initialize(req, res)
|
11
|
-
@req
|
12
|
-
@res
|
11
|
+
@req = req
|
12
|
+
@res = res
|
13
13
|
res.headers['content-type'] = 'text/html; charset=utf-8'
|
14
14
|
end
|
15
15
|
|
@@ -21,15 +21,15 @@ class App
|
|
21
21
|
<p>Minimal Ruby web framework with style</p>
|
22
22
|
</div>
|
23
23
|
|
24
|
-
#{otto_card(
|
24
|
+
#{otto_card('Send Feedback') do
|
25
25
|
otto_form_wrapper do
|
26
|
-
otto_input(
|
27
|
-
otto_button(
|
26
|
+
otto_input('msg', placeholder: 'Your message...') +
|
27
|
+
otto_button('Send Feedback')
|
28
28
|
end
|
29
29
|
end}
|
30
30
|
HTML
|
31
31
|
|
32
|
-
res.
|
32
|
+
res.send_secure_cookie :sess, 1_234_567, 3600
|
33
33
|
res.body = otto_page(content)
|
34
34
|
end
|
35
35
|
|
@@ -37,14 +37,14 @@ class App
|
|
37
37
|
message = req.params['msg']&.strip
|
38
38
|
|
39
39
|
content = if message.nil? || message.empty?
|
40
|
-
otto_alert(
|
40
|
+
otto_alert('error', 'Empty Message', 'Please enter a message before submitting.')
|
41
41
|
else
|
42
|
-
otto_alert(
|
43
|
-
|
42
|
+
otto_alert('success', 'Feedback Received', 'Thanks for your message!') +
|
43
|
+
otto_card('Your Message') { otto_code_block(message, 'text') }
|
44
44
|
end
|
45
45
|
|
46
|
-
content += "<p>#{otto_link(
|
47
|
-
res.body = otto_page(content,
|
46
|
+
content += "<p>#{otto_link('← Back', '/')}</p>"
|
47
|
+
res.body = otto_page(content, 'Feedback')
|
48
48
|
end
|
49
49
|
|
50
50
|
def redirect
|
@@ -53,26 +53,26 @@ class App
|
|
53
53
|
|
54
54
|
def robots_text
|
55
55
|
res.headers['content-type'] = 'text/plain'
|
56
|
-
res.body
|
56
|
+
res.body = ['User-agent: *', 'Disallow: /private'].join($/)
|
57
57
|
end
|
58
58
|
|
59
59
|
def display_product
|
60
60
|
res.headers['content-type'] = 'application/json; charset=utf-8'
|
61
|
-
prodid
|
62
|
-
res.body
|
61
|
+
prodid = req.params[:prodid]
|
62
|
+
res.body = format('{"product":%s,"msg":"Hint: try another value"}', prodid)
|
63
63
|
end
|
64
64
|
|
65
65
|
def not_found
|
66
66
|
res.status = 404
|
67
|
-
content
|
68
|
-
content
|
69
|
-
res.body
|
67
|
+
content = otto_alert('error', 'Not Found', 'The requested page could not be found.')
|
68
|
+
content += "<p>#{otto_link('← Home', '/')}</p>"
|
69
|
+
res.body = otto_page(content, '404')
|
70
70
|
end
|
71
71
|
|
72
72
|
def server_error
|
73
73
|
res.status = 500
|
74
|
-
content
|
75
|
-
content
|
76
|
-
res.body
|
74
|
+
content = otto_alert('error', 'Server Error', 'An internal server error occurred.')
|
75
|
+
content += "<p>#{otto_link('← Home', '/')}</p>"
|
76
|
+
res.body = otto_page(content, '500')
|
77
77
|
end
|
78
78
|
end
|
data/examples/basic/config.ru
CHANGED
@@ -8,8 +8,8 @@ class App
|
|
8
8
|
attr_reader :req, :res
|
9
9
|
|
10
10
|
def initialize(req, res)
|
11
|
-
@req
|
12
|
-
@res
|
11
|
+
@req = req
|
12
|
+
@res = res
|
13
13
|
res.headers['content-type'] = 'text/html; charset=utf-8'
|
14
14
|
end
|
15
15
|
|
@@ -23,19 +23,19 @@ class App
|
|
23
23
|
<p>Minimal Ruby web framework with style</p>
|
24
24
|
</div>
|
25
25
|
|
26
|
-
#{otto_card(
|
26
|
+
#{otto_card('Dynamic Pages') do
|
27
27
|
# This is a nested heredoc within the outer HTML heredoc
|
28
28
|
# It uses a different delimiter (EXAMPLES) to avoid conflicts
|
29
29
|
# The #{} interpolation allows the inner heredoc to be embedded
|
30
30
|
<<~EXAMPLES
|
31
|
-
#{otto_link(
|
32
|
-
#{otto_link(
|
33
|
-
#{otto_link(
|
31
|
+
#{otto_link('Product #100', '/product/100')} - View product page<br>
|
32
|
+
#{otto_link('Product #42', '/product/42')} - Different product<br>
|
33
|
+
#{otto_link('API Data', '/product/100.json')} - JSON endpoint
|
34
34
|
EXAMPLES
|
35
35
|
end}
|
36
36
|
HTML
|
37
37
|
|
38
|
-
res.
|
38
|
+
res.send_secure_cookie :sess, 1_234_567, 3600
|
39
39
|
res.body = otto_page(content)
|
40
40
|
end
|
41
41
|
|
@@ -43,14 +43,14 @@ class App
|
|
43
43
|
message = req.params['msg']&.strip
|
44
44
|
|
45
45
|
content = if message.nil? || message.empty?
|
46
|
-
otto_alert(
|
46
|
+
otto_alert('error', 'Empty Message', 'Please enter a message before submitting.')
|
47
47
|
else
|
48
|
-
otto_alert(
|
49
|
-
|
48
|
+
otto_alert('success', 'Feedback Received', 'Thanks for your message!') +
|
49
|
+
otto_card('Your Message') { otto_code_block(message, 'text') }
|
50
50
|
end
|
51
51
|
|
52
|
-
content += "<p>#{otto_link(
|
53
|
-
res.body = otto_page(content,
|
52
|
+
content += "<p>#{otto_link('← Back', '/')}</p>"
|
53
|
+
res.body = otto_page(content, 'Feedback')
|
54
54
|
end
|
55
55
|
|
56
56
|
def redirect
|
@@ -59,7 +59,7 @@ class App
|
|
59
59
|
|
60
60
|
def robots_text
|
61
61
|
res.headers['content-type'] = 'text/plain'
|
62
|
-
res.body
|
62
|
+
res.body = ['User-agent: *', 'Disallow: /private'].join($/)
|
63
63
|
end
|
64
64
|
|
65
65
|
def display_product
|
@@ -71,27 +71,27 @@ class App
|
|
71
71
|
|
72
72
|
if wants_json
|
73
73
|
res.headers['content-type'] = 'application/json; charset=utf-8'
|
74
|
-
res.body
|
74
|
+
res.body = format('{"product":%s,"msg":"Hint: try another value"}', prodid)
|
75
75
|
else
|
76
76
|
# Return HTML product page
|
77
77
|
product_data = {
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
78
|
+
'Product ID' => prodid,
|
79
|
+
'Name' => "Sample Product ##{prodid}",
|
80
|
+
'Price' => "$#{rand(10..999)}.99",
|
81
|
+
'Description' => 'This is a demonstration product showing dynamic routing with parameter :prodid',
|
82
|
+
'Stock' => rand(0..50) > 5 ? 'In Stock' : 'Out of Stock',
|
83
83
|
}
|
84
84
|
|
85
|
-
product_html = product_data.map
|
85
|
+
product_html = product_data.map do |key, value|
|
86
86
|
"<p><strong>#{key}:</strong> #{escape_html(value.to_s)}</p>"
|
87
|
-
|
87
|
+
end.join
|
88
88
|
|
89
89
|
content = <<~HTML
|
90
|
-
#{otto_card(
|
90
|
+
#{otto_card('Product Details') { product_html }}
|
91
91
|
|
92
92
|
<p>
|
93
|
-
#{otto_link(
|
94
|
-
#{otto_link(
|
93
|
+
#{otto_link('← Back to Home', '/')} |
|
94
|
+
#{otto_link('View as JSON', "/product/#{prodid}.json")}
|
95
95
|
</p>
|
96
96
|
HTML
|
97
97
|
|
@@ -101,15 +101,15 @@ class App
|
|
101
101
|
|
102
102
|
def not_found
|
103
103
|
res.status = 404
|
104
|
-
content
|
105
|
-
content
|
106
|
-
res.body
|
104
|
+
content = otto_alert('error', 'Not Found', 'The requested page could not be found.')
|
105
|
+
content += "<p>#{otto_link('← Home', '/')}</p>"
|
106
|
+
res.body = otto_page(content, '404')
|
107
107
|
end
|
108
108
|
|
109
109
|
def server_error
|
110
110
|
res.status = 500
|
111
|
-
content
|
112
|
-
content
|
113
|
-
res.body
|
111
|
+
content = otto_alert('error', 'Server Error', 'An internal server error occurred.')
|
112
|
+
content += "<p>#{otto_link('← Home', '/')}</p>"
|
113
|
+
res.body = otto_page(content, '500')
|
114
114
|
end
|
115
115
|
end
|
@@ -9,8 +9,8 @@ class SecureApp
|
|
9
9
|
attr_reader :req, :res
|
10
10
|
|
11
11
|
def initialize(req, res)
|
12
|
-
@req
|
13
|
-
@res
|
12
|
+
@req = req
|
13
|
+
@res = res
|
14
14
|
res.headers['content-type'] = 'text/html; charset=utf-8'
|
15
15
|
end
|
16
16
|
|
@@ -28,49 +28,47 @@ class SecureApp
|
|
28
28
|
<p class="otto-mb-md">Security demonstration for the Otto framework</p>
|
29
29
|
</div>
|
30
30
|
|
31
|
-
#{otto_card(
|
32
|
-
|
31
|
+
#{otto_card('CSRF Protected Feedback') do
|
33
32
|
<<~FORM
|
34
33
|
<form method="post" action="/feedback" class="otto-form">
|
35
34
|
#{csrf_tag}
|
36
35
|
<label>Message:</label>
|
37
|
-
#{otto_textarea(
|
38
|
-
#{otto_button(
|
36
|
+
#{otto_textarea('message', placeholder: 'Enter your feedback...', required: true)}
|
37
|
+
#{otto_button('Submit Feedback', variant: 'primary')}
|
39
38
|
</form>
|
40
39
|
FORM
|
41
|
-
|
42
40
|
end}
|
43
41
|
|
44
|
-
#{otto_card(
|
42
|
+
#{otto_card('File Upload Validation') do
|
45
43
|
<<~UPLOAD
|
46
44
|
<form method="post" action="/upload" enctype="multipart/form-data" class="otto-form">
|
47
45
|
#{csrf_tag}
|
48
46
|
<label>Choose file:</label>
|
49
47
|
<input type="file" name="upload_file" class="otto-input">
|
50
|
-
#{otto_button(
|
48
|
+
#{otto_button('Upload File', variant: 'primary')}
|
51
49
|
</form>
|
52
50
|
UPLOAD
|
53
51
|
end}
|
54
52
|
|
55
|
-
#{otto_card(
|
53
|
+
#{otto_card('User Profile Input Validation') do
|
56
54
|
<<~PROFILE
|
57
55
|
<form method="post" action="/profile" class="otto-form">
|
58
56
|
#{csrf_tag}
|
59
57
|
<label>Name:</label>
|
60
|
-
#{otto_input(
|
58
|
+
#{otto_input('name', placeholder: 'Your name', required: true)}
|
61
59
|
|
62
60
|
<label>Email:</label>
|
63
|
-
#{otto_input(
|
61
|
+
#{otto_input('email', type: 'email', placeholder: 'your@email.com', required: true)}
|
64
62
|
|
65
63
|
<label>Bio:</label>
|
66
|
-
#{otto_textarea(
|
64
|
+
#{otto_textarea('bio', placeholder: 'Tell us about yourself...')}
|
67
65
|
|
68
|
-
#{otto_button(
|
66
|
+
#{otto_button('Update Profile', variant: 'primary')}
|
69
67
|
</form>
|
70
68
|
PROFILE
|
71
69
|
end}
|
72
70
|
|
73
|
-
#{otto_card(
|
71
|
+
#{otto_card('Security Information') do
|
74
72
|
<<~INFO
|
75
73
|
<h3>Security Features Active:</h3>
|
76
74
|
<ul>
|
@@ -87,13 +85,13 @@ class SecureApp
|
|
87
85
|
</p>
|
88
86
|
|
89
87
|
<p class="otto-mt-md">
|
90
|
-
#{otto_link(
|
88
|
+
#{otto_link('View Request Headers', '/headers')}
|
91
89
|
</p>
|
92
90
|
INFO
|
93
91
|
end}
|
94
92
|
HTML
|
95
93
|
|
96
|
-
res.body = otto_page(content,
|
94
|
+
res.body = otto_page(content, 'Otto Security Features')
|
97
95
|
end
|
98
96
|
|
99
97
|
def receive_feedback
|
@@ -104,29 +102,28 @@ class SecureApp
|
|
104
102
|
safe_message = validate_input(message, max_length: 1000, allow_html: false)
|
105
103
|
else
|
106
104
|
safe_message = message.to_s.strip
|
107
|
-
raise
|
105
|
+
raise 'Message too long' if safe_message.length > 1000
|
108
106
|
end
|
109
107
|
|
110
|
-
if safe_message.empty?
|
111
|
-
|
108
|
+
content = if safe_message.empty?
|
109
|
+
otto_alert('error', 'Validation Error', 'Message cannot be empty.')
|
112
110
|
else
|
113
|
-
|
114
|
-
#{otto_alert(
|
111
|
+
<<~HTML
|
112
|
+
#{otto_alert('success', 'Feedback Received', 'Thank you for your feedback!')}
|
115
113
|
|
116
|
-
#{otto_card(
|
114
|
+
#{otto_card('Your Message') do
|
117
115
|
otto_code_block(safe_message, 'text')
|
118
116
|
end}
|
119
117
|
HTML
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
content = otto_alert("error", "Processing Error", "An error occurred processing your request.")
|
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.')
|
126
123
|
end
|
127
124
|
|
128
|
-
content += "<p class=\"otto-mt-lg\">#{otto_link(
|
129
|
-
res.body = otto_page(content,
|
125
|
+
content += "<p class=\"otto-mt-lg\">#{otto_link('← Back to form', '/')}</p>"
|
126
|
+
res.body = otto_page(content, 'Feedback Response')
|
130
127
|
end
|
131
128
|
|
132
129
|
def upload_file
|
@@ -134,31 +131,39 @@ class SecureApp
|
|
134
131
|
uploaded_file = req.params['upload_file']
|
135
132
|
|
136
133
|
if uploaded_file.nil? || uploaded_file.empty?
|
137
|
-
content = otto_alert(
|
134
|
+
content = otto_alert('error', 'Upload Error', 'No file was selected.')
|
138
135
|
else
|
139
|
-
|
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
|
140
145
|
|
141
|
-
if respond_to?(:sanitize_filename)
|
142
|
-
|
146
|
+
safe_filename = if respond_to?(:sanitize_filename)
|
147
|
+
sanitize_filename(filename)
|
143
148
|
else
|
144
|
-
|
145
|
-
|
149
|
+
File.basename(filename.to_s).gsub(/[^\w\-_\.]/, '_')
|
150
|
+
end
|
146
151
|
|
147
152
|
file_info = {
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
153
|
+
'Original filename' => filename,
|
154
|
+
'Sanitized filename' => safe_filename,
|
155
|
+
'Content type' => uploaded_file[:type] || 'unknown',
|
156
|
+
'Security status' => 'File validated and processed safely',
|
152
157
|
}
|
153
158
|
|
154
|
-
info_html = file_info.map
|
159
|
+
info_html = file_info.map do |key, value|
|
155
160
|
"<p><strong>#{key}:</strong> #{escape_html(value)}</p>"
|
156
|
-
|
161
|
+
end.join
|
157
162
|
|
158
163
|
content = <<~HTML
|
159
|
-
#{otto_alert(
|
164
|
+
#{otto_alert('success', 'File Upload Successful', 'File processed and validated successfully!')}
|
160
165
|
|
161
|
-
#{otto_card(
|
166
|
+
#{otto_card('File Information') do
|
162
167
|
info_html
|
163
168
|
end}
|
164
169
|
|
@@ -168,64 +173,62 @@ class SecureApp
|
|
168
173
|
</div>
|
169
174
|
HTML
|
170
175
|
end
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
content = otto_alert("error", "Upload Error", "An error occurred during file upload.")
|
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.')
|
176
180
|
end
|
177
181
|
|
178
|
-
content += "<p class=\"otto-mt-lg\">#{otto_link(
|
179
|
-
res.body = otto_page(content,
|
182
|
+
content += "<p class=\"otto-mt-lg\">#{otto_link('← Back to form', '/')}</p>"
|
183
|
+
res.body = otto_page(content, 'Upload Response')
|
180
184
|
end
|
181
185
|
|
182
186
|
def update_profile
|
183
187
|
begin
|
184
|
-
name
|
188
|
+
name = req.params['name']
|
185
189
|
email = req.params['email']
|
186
|
-
bio
|
190
|
+
bio = req.params['bio']
|
187
191
|
|
188
192
|
if respond_to?(:validate_input)
|
189
|
-
safe_name
|
193
|
+
safe_name = validate_input(name, max_length: 100)
|
190
194
|
safe_email = validate_input(email, max_length: 255)
|
191
|
-
safe_bio
|
195
|
+
safe_bio = validate_input(bio, max_length: 500, allow_html: false)
|
192
196
|
else
|
193
|
-
safe_name
|
197
|
+
safe_name = name.to_s.strip[0..99]
|
194
198
|
safe_email = email.to_s.strip[0..254]
|
195
|
-
safe_bio
|
199
|
+
safe_bio = bio.to_s.strip[0..499]
|
196
200
|
end
|
197
201
|
|
198
202
|
unless safe_email.match?(/\A[^@\s]+@[^@\s]+\z/)
|
199
|
-
raise Otto::Security::ValidationError,
|
203
|
+
raise Otto::Security::ValidationError, 'Invalid email format'
|
200
204
|
end
|
201
205
|
|
202
206
|
profile_data = {
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
+
'Name' => safe_name,
|
208
|
+
'Email' => safe_email,
|
209
|
+
'Bio' => safe_bio,
|
210
|
+
'Updated' => Time.now.strftime('%Y-%m-%d %H:%M:%S UTC'),
|
207
211
|
}
|
208
212
|
|
209
|
-
profile_html = profile_data.map
|
213
|
+
profile_html = profile_data.map do |key, value|
|
210
214
|
"<p><strong>#{key}:</strong> #{escape_html(value)}</p>"
|
211
|
-
|
215
|
+
end.join
|
212
216
|
|
213
217
|
content = <<~HTML
|
214
|
-
#{otto_alert(
|
218
|
+
#{otto_alert('success', 'Profile Updated', 'Your profile has been updated successfully!')}
|
215
219
|
|
216
|
-
#{otto_card(
|
220
|
+
#{otto_card('Profile Data') do
|
217
221
|
profile_html
|
218
222
|
end}
|
219
223
|
HTML
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
content = otto_alert("error", "Update Error", "An error occurred updating your profile.")
|
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.')
|
225
228
|
end
|
226
229
|
|
227
|
-
content += "<p class=\"otto-mt-lg\">#{otto_link(
|
228
|
-
res.body = otto_page(content,
|
230
|
+
content += "<p class=\"otto-mt-lg\">#{otto_link('← Back to form', '/')}</p>"
|
231
|
+
res.body = otto_page(content, 'Profile Update')
|
229
232
|
end
|
230
233
|
|
231
234
|
def show_headers
|
@@ -239,16 +242,16 @@ class SecureApp
|
|
239
242
|
end
|
240
243
|
|
241
244
|
response_data = {
|
242
|
-
message:
|
245
|
+
message: 'Request headers analysis (filtered for security)',
|
243
246
|
client_ip: req.respond_to?(:client_ipaddress) ? req.client_ipaddress : req.ip,
|
244
247
|
secure_connection: req.respond_to?(:secure?) ? req.secure? : false,
|
245
248
|
timestamp: Time.now.utc.iso8601,
|
246
249
|
headers: safe_headers,
|
247
250
|
security_analysis: {
|
248
|
-
csrf_protection: respond_to?(:csrf_token_valid?) ?
|
249
|
-
content_security:
|
250
|
-
xss_protection:
|
251
|
-
}
|
251
|
+
csrf_protection: respond_to?(:csrf_token_valid?) ? 'Active' : 'Basic',
|
252
|
+
content_security: 'Headers validated and filtered',
|
253
|
+
xss_protection: 'HTML escaping enabled',
|
254
|
+
},
|
252
255
|
}
|
253
256
|
|
254
257
|
require 'json'
|
@@ -257,17 +260,17 @@ class SecureApp
|
|
257
260
|
|
258
261
|
def not_found
|
259
262
|
res.status = 404
|
260
|
-
content
|
261
|
-
content
|
262
|
-
res.body
|
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')
|
263
266
|
end
|
264
267
|
|
265
268
|
def server_error
|
266
269
|
res.status = 500
|
267
|
-
error_id
|
268
|
-
content
|
269
|
-
content
|
270
|
-
content
|
271
|
-
res.body
|
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')
|
272
275
|
end
|
273
276
|
end
|
@@ -13,7 +13,7 @@ require_relative '../../lib/otto'
|
|
13
13
|
require_relative 'app'
|
14
14
|
|
15
15
|
# Create Otto app with security features enabled
|
16
|
-
app = Otto.new(
|
16
|
+
app = Otto.new('./routes', {
|
17
17
|
# Enable CSRF protection for POST, PUT, DELETE requests
|
18
18
|
csrf_protection: true,
|
19
19
|
|
@@ -42,14 +42,15 @@ app = Otto.new("./routes", {
|
|
42
42
|
security_headers: {
|
43
43
|
'content-security-policy' => "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'",
|
44
44
|
'strict-transport-security' => 'max-age=31536000; includeSubDomains',
|
45
|
-
'x-frame-options' => 'DENY'
|
46
|
-
}
|
47
|
-
}
|
45
|
+
'x-frame-options' => 'DENY',
|
46
|
+
},
|
47
|
+
}
|
48
|
+
)
|
48
49
|
|
49
50
|
# Optional: Configure additional security settings
|
50
51
|
app.security_config.max_request_size = 5 * 1024 * 1024 # 5MB limit
|
51
|
-
app.security_config.max_param_depth
|
52
|
-
app.security_config.max_param_keys
|
52
|
+
app.security_config.max_param_depth = 10 # Limit parameter nesting
|
53
|
+
app.security_config.max_param_keys = 50 # Limit parameters per request
|
53
54
|
|
54
55
|
# Optional: Add static file serving with security
|
55
56
|
app.option[:public] = public_path
|
@@ -62,20 +63,21 @@ if ENV['RACK_ENV'] == 'production'
|
|
62
63
|
# More restrictive CSP for production
|
63
64
|
app.set_security_headers({
|
64
65
|
'content-security-policy' => "default-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'",
|
65
|
-
'strict-transport-security' => 'max-age=63072000; includeSubDomains; preload'
|
66
|
-
}
|
66
|
+
'strict-transport-security' => 'max-age=63072000; includeSubDomains; preload',
|
67
|
+
},
|
68
|
+
)
|
67
69
|
else
|
68
70
|
# Development-specific settings
|
69
|
-
puts
|
70
|
-
puts
|
71
|
-
puts
|
72
|
-
puts
|
73
|
-
puts
|
74
|
-
puts
|
75
|
-
puts
|
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 ''
|
76
78
|
end
|
77
79
|
|
78
80
|
# Mount the application
|
79
|
-
map('/')
|
81
|
+
map('/') do
|
80
82
|
run app
|
81
|
-
|
83
|
+
end
|