otto 2.8.1 → 2.10.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/.github/dependabot.yml +5 -0
- data/.github/workflows/ci.yml +11 -8
- data/.github/workflows/claude-code-review.yml +38 -13
- data/.github/workflows/claude.yml +10 -8
- data/.github/workflows/code-smells.yml +5 -5
- data/.github/workflows/release-gem.yml +2 -2
- data/.github/workflows/ruby-lint.yml +3 -3
- data/.github/workflows/yardoc.yml +5 -5
- data/.gitignore +1 -5
- data/.rubocop_todo.yml +7 -5
- data/AGENTS.md +22 -1
- data/CHANGELOG.rst +239 -0
- data/Gemfile +3 -3
- data/Gemfile.lock +11 -15
- data/README.md +139 -38
- data/docs/README.md +166 -0
- data/docs/adr/README.md +16 -0
- data/docs/adr/adr-001-route-authentication-at-handler-boundary.md +38 -0
- data/docs/adr/adr-002-multi-strategy-authentication-and-authorization.md +50 -0
- data/docs/adr/adr-003-caddy-tls-route-based-integration.md +48 -0
- data/docs/adr/adr-004-separate-compatibility-from-security-maintenance.md +58 -0
- data/docs/guides/authentication.md +377 -0
- data/docs/guides/caddy-tls.md +205 -0
- data/docs/guides/configuration_freezing.md +146 -0
- data/docs/guides/enrichment.md +161 -0
- data/docs/guides/forwarded-authority.md +249 -0
- data/docs/guides/geo-country.md +168 -0
- data/docs/guides/ip_privacy.md +39 -0
- data/docs/guides/ipaddr-encoding-quirk.md +56 -0
- data/docs/guides/mcp.md +282 -0
- data/docs/guides/privacy.md +193 -0
- data/docs/guides/routing.md +181 -0
- data/docs/guides/structured_logging.md +281 -0
- data/docs/guides/testing-guide.md +391 -0
- data/docs/maintainers/github-actions.md +41 -0
- data/docs/maintainers/investigations/.gitignore +2 -0
- data/docs/migrating/v2.0.0.md +337 -0
- data/docs/reference/authentication.md +290 -0
- data/docs/reference/route-syntax.md +181 -0
- data/docs/reference/runtime-and-dependency-security.md +86 -0
- data/examples/advanced_routes/README.md +43 -57
- data/examples/authentication_strategies/README.md +37 -196
- data/examples/basic/README.md +24 -39
- data/examples/basic/config.ru +0 -1
- data/examples/caddy_tls_demo/README.md +8 -2
- data/examples/lambda_handlers/README.md +11 -2
- data/examples/mcp_demo/README.md +75 -161
- data/examples/mcp_demo/config.ru +1 -0
- data/examples/security_features/README.md +36 -234
- data/lib/otto/caddy_tls/localhost_guard.rb +22 -24
- data/lib/otto/core/configuration.rb +36 -22
- data/lib/otto/core/file_safety.rb +89 -31
- data/lib/otto/core/middleware_stack.rb +36 -36
- data/lib/otto/core/router.rb +62 -19
- data/lib/otto/env_keys.rb +42 -2
- data/lib/otto/mcp/auth/token.rb +10 -4
- data/lib/otto/mcp/core.rb +23 -5
- data/lib/otto/mcp/endpoint.rb +41 -0
- data/lib/otto/mcp/errors.rb +15 -0
- data/lib/otto/mcp/options.rb +292 -0
- data/lib/otto/mcp/protocol.rb +52 -22
- data/lib/otto/mcp/rate_limiting.rb +175 -97
- data/lib/otto/mcp/registry.rb +14 -14
- data/lib/otto/mcp/schema_validation.rb +20 -12
- data/lib/otto/mcp/server.rb +131 -32
- data/lib/otto/optional_dependency.rb +57 -0
- data/lib/otto/privacy/config.rb +10 -8
- data/lib/otto/response.rb +7 -1
- data/lib/otto/security/authentication/route_auth_wrapper/role_authorization.rb +22 -5
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +181 -18
- data/lib/otto/security/authentication/strategies/permission_strategy.rb +1 -0
- data/lib/otto/security/authentication/strategies/role_strategy.rb +1 -0
- data/lib/otto/security/authentication/strategies/session_strategy.rb +1 -0
- data/lib/otto/security/authentication/strategy_result.rb +58 -28
- data/lib/otto/security/config.rb +379 -20
- data/lib/otto/security/configurator.rb +36 -6
- data/lib/otto/security/core.rb +19 -1
- data/lib/otto/security/csp/emit_middleware.rb +2 -1
- data/lib/otto/security/csp/policy.rb +159 -7
- data/lib/otto/security/csp/request_extras.rb +256 -0
- data/lib/otto/security/csp/writer.rb +153 -12
- data/lib/otto/security/csp.rb +5 -0
- data/lib/otto/security/middleware/ip_privacy_middleware.rb +105 -4
- data/lib/otto/security/middleware/rate_limit_middleware.rb +1 -6
- data/lib/otto/security/rate_limiter.rb +81 -46
- data/lib/otto/utils.rb +50 -0
- data/lib/otto/version.rb +1 -1
- data/lib/otto.rb +9 -11
- data/otto.gemspec +0 -2
- metadata +32 -41
- data/docs/.gitignore +0 -10
- data/docs/1108-STREAMING_ARCHITECTURE_ANALYSIS.md +0 -1105
- data/docs/1108-STREAMING_SUPPORT_SUMMARY.md +0 -376
- data/docs/enrichment.md +0 -128
- data/docs/geo-country.md +0 -181
- data/docs/ipaddr-encoding-quirk.md +0 -34
- data/docs/migrating/v2.0.0-pre1.md +0 -276
- data/docs/migrating/v2.0.0-pre2.md +0 -338
- data/docs/modern-authentication-authorization-landscape.md +0 -558
- data/docs/multi-strategy-authentication-design.md +0 -1401
- data/docs/reverse-proxy-network-services.md +0 -371
data/examples/mcp_demo/README.md
CHANGED
|
@@ -1,75 +1,96 @@
|
|
|
1
1
|
# Otto MCP Demo
|
|
2
2
|
|
|
3
|
-
This example
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This example boots Otto's Model Context Protocol (MCP) JSON-RPC 2.0 endpoint at
|
|
4
|
+
`/_mcp`. Use it to verify endpoint initialization alongside ordinary Otto web
|
|
5
|
+
routes.
|
|
6
6
|
|
|
7
7
|
## What You'll Learn
|
|
8
8
|
|
|
9
|
-
- How to
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- How MCP integrates with your existing Otto application
|
|
9
|
+
- How to enable an MCP HTTP endpoint
|
|
10
|
+
- How the endpoint coexists with ordinary Otto web routes
|
|
11
|
+
- How to send a JSON-RPC 2.0 `initialize` request
|
|
12
|
+
- How bearer-token authentication protects the endpoint
|
|
13
|
+
- The current resource and tool limitations described below
|
|
15
14
|
|
|
16
15
|
## Features Demonstrated
|
|
17
16
|
|
|
18
|
-
- **MCP
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **Resources**: Read-only data exposed via `MCP` routes
|
|
22
|
-
- **Tools**: Executable actions exposed via `TOOL` routes
|
|
23
|
-
- **Web Interface**: Separate web routes coexist with MCP routes
|
|
24
|
-
- **JSON-RPC 2.0**: Standard protocol for all MCP interactions
|
|
17
|
+
- **MCP endpoint**: A single `POST /_mcp` endpoint
|
|
18
|
+
- **Web interface**: Separate web routes coexist with the MCP endpoint
|
|
19
|
+
- **JSON-RPC 2.0**: `initialize`, `resources/list`, and `tools/list` requests
|
|
25
20
|
|
|
26
21
|
## How to Run
|
|
27
22
|
|
|
28
|
-
|
|
23
|
+
Run this example from an Otto source checkout. It requires Ruby 3.2 through
|
|
24
|
+
4.0, Bundler, and the development dependencies: `rackup` is a development
|
|
25
|
+
dependency in the root `Gemfile` and is not installed with the released `otto`
|
|
26
|
+
gem. MCP enables schema validation and rate limiting by default. Those features
|
|
27
|
+
require `json_schemer` 2.0.0 or newer in the 2.x series and `rack-attack` 6.7.0
|
|
28
|
+
or newer in the 6.x series:
|
|
29
29
|
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
```ruby
|
|
31
|
+
# Gemfile
|
|
32
|
+
gem 'json_schemer', '~> 2.0'
|
|
33
|
+
gem 'rack-attack', '~> 6.7'
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
If either enabled feature's gem is missing or incompatible, MCP setup raises
|
|
37
|
+
`Otto::OptionalDependencyError` during configuration. Pass
|
|
38
|
+
`enable_validation: false` or `enable_rate_limiting: false` to `enable_mcp!`, or
|
|
39
|
+
alongside `mcp_enabled: true` in the `Otto.new` options, only when that protection
|
|
40
|
+
is intentionally disabled. To enforce the configured limits, mount
|
|
41
|
+
`Rack::Attack` before Otto in `config.ru`, as this example does.
|
|
36
42
|
|
|
37
43
|
```sh
|
|
44
|
+
cd /path/to/otto
|
|
45
|
+
bundle config set with development
|
|
46
|
+
bundle install
|
|
38
47
|
cd examples/mcp_demo
|
|
39
|
-
|
|
48
|
+
bundle exec rackup config.ru
|
|
40
49
|
```
|
|
41
50
|
|
|
42
|
-
The server will start on `http://localhost:9292`.
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
The server listens at `http://localhost:9292` by default.
|
|
53
|
+
|
|
54
|
+
- **Web interface**: Open `http://localhost:9292/`.
|
|
55
|
+
- **Health check**: `curl -i http://localhost:9292/health` returns `200` and `OK`.
|
|
56
|
+
- **MCP endpoint**: Send JSON-RPC 2.0 requests to `http://localhost:9292/_mcp`.
|
|
46
57
|
|
|
47
58
|
## Authentication
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
`config.ru` configures two bearer tokens, and Otto enforces them: every request
|
|
61
|
+
to `/_mcp` must send `Authorization: Bearer demo-token-123` (or
|
|
62
|
+
`X-MCP-Token: demo-token-123`). Requests without a valid token get HTTP `401`
|
|
63
|
+
and a JSON-RPC `Unauthorized` error. The `requests_per_minute` and
|
|
64
|
+
`tools_per_minute` values in `config.ru` are applied as configured. See the
|
|
65
|
+
[MCP guide](../../docs/guides/mcp.md) for the full option list.
|
|
66
|
+
|
|
67
|
+
## Current Limitations
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
Although `routes` contains `MCP /users` and `TOOL /create_user` declarations,
|
|
70
|
+
the current route-loading path does not register them. Therefore
|
|
71
|
+
`resources/list` and `tools/list` both return empty arrays, and `resources/read`
|
|
72
|
+
or `tools/call` for those names fails. This README documents the runnable
|
|
73
|
+
endpoint behavior; do not use this example as a resource or tool integration
|
|
74
|
+
template until those routes are registered.
|
|
54
75
|
|
|
55
76
|
## Interacting with the MCP Endpoint
|
|
56
77
|
|
|
57
78
|
All MCP interactions use the `POST /_mcp` endpoint. Each request is a JSON-RPC 2.0 request with:
|
|
58
79
|
|
|
59
80
|
- `jsonrpc`: Always `"2.0"`
|
|
60
|
-
- `method`: The RPC method name
|
|
81
|
+
- `method`: The RPC method name
|
|
61
82
|
- `id`: Request ID (for matching responses)
|
|
62
83
|
- `params`: Optional parameters as an object
|
|
63
84
|
|
|
64
85
|
Required headers:
|
|
65
|
-
- `Authorization: Bearer <token>`
|
|
66
86
|
- `Content-Type: application/json`
|
|
87
|
+
- `Authorization: Bearer demo-token-123` (or `X-MCP-Token: demo-token-123`)
|
|
67
88
|
|
|
68
89
|
Example:
|
|
69
90
|
```sh
|
|
70
91
|
curl -X POST http://localhost:9292/_mcp \
|
|
71
|
-
-H 'Authorization: Bearer demo-token-123' \
|
|
72
92
|
-H 'Content-Type: application/json' \
|
|
93
|
+
-H 'Authorization: Bearer demo-token-123' \
|
|
73
94
|
-d '{
|
|
74
95
|
"jsonrpc": "2.0",
|
|
75
96
|
"method": "initialize",
|
|
@@ -84,156 +105,53 @@ The `initialize` method is a built-in MCP method that returns information about
|
|
|
84
105
|
|
|
85
106
|
```sh
|
|
86
107
|
curl -X POST http://localhost:9292/_mcp \
|
|
87
|
-
-H 'Authorization: Bearer demo-token-123' \
|
|
88
108
|
-H 'Content-Type: application/json' \
|
|
109
|
+
-H 'Authorization: Bearer demo-token-123' \
|
|
89
110
|
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{}}'
|
|
90
111
|
```
|
|
91
112
|
|
|
92
|
-
|
|
113
|
+
## Verification
|
|
93
114
|
|
|
94
|
-
|
|
115
|
+
A successful `initialize` request returns `result.protocolVersion`,
|
|
116
|
+
`result.capabilities`, and `result.serverInfo` with the same request ID. To
|
|
117
|
+
confirm the current registry state, run:
|
|
95
118
|
|
|
96
119
|
```sh
|
|
97
120
|
curl -X POST http://localhost:9292/_mcp \
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### Tool: Create User
|
|
104
|
-
|
|
105
|
-
This calls the `UserAPI.mcp_create_user` method defined as a `TOOL` route. The method name is derived from the route path (`/create_user` -> `create_user`).
|
|
121
|
+
-H 'Content-Type: application/json' \
|
|
122
|
+
-H 'Authorization: Bearer demo-token-123' \
|
|
123
|
+
-d '{"jsonrpc":"2.0","method":"resources/list","id":2}'
|
|
106
124
|
|
|
107
|
-
```sh
|
|
108
125
|
curl -X POST http://localhost:9292/_mcp \
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"jsonrpc": "2.0",
|
|
113
|
-
"method": "create_user",
|
|
114
|
-
"id": 3,
|
|
115
|
-
"params": {
|
|
116
|
-
"name": "Charlie",
|
|
117
|
-
"email": "charlie@example.com"
|
|
118
|
-
}
|
|
119
|
-
}'
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## Expected Output
|
|
123
|
-
|
|
124
|
-
### Successful Initialize Request
|
|
125
|
-
```json
|
|
126
|
-
{
|
|
127
|
-
"jsonrpc": "2.0",
|
|
128
|
-
"result": {
|
|
129
|
-
"resources": [
|
|
130
|
-
{
|
|
131
|
-
"uri": "users",
|
|
132
|
-
"name": "User List",
|
|
133
|
-
"description": "List all users"
|
|
134
|
-
}
|
|
135
|
-
],
|
|
136
|
-
"tools": [
|
|
137
|
-
{
|
|
138
|
-
"name": "create_user",
|
|
139
|
-
"description": "Create a new user",
|
|
140
|
-
"inputSchema": {
|
|
141
|
-
"type": "object",
|
|
142
|
-
"properties": {
|
|
143
|
-
"name": { "type": "string" },
|
|
144
|
-
"email": { "type": "string" }
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
]
|
|
149
|
-
},
|
|
150
|
-
"id": 1
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### Successful Resource Request
|
|
155
|
-
```json
|
|
156
|
-
{
|
|
157
|
-
"jsonrpc": "2.0",
|
|
158
|
-
"result": {
|
|
159
|
-
"users": [
|
|
160
|
-
{ "id": 1, "name": "Alice", "email": "alice@example.com" },
|
|
161
|
-
{ "id": 2, "name": "Bob", "email": "bob@example.com" }
|
|
162
|
-
]
|
|
163
|
-
},
|
|
164
|
-
"id": 2
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Successful Tool Execution
|
|
169
|
-
```json
|
|
170
|
-
{
|
|
171
|
-
"jsonrpc": "2.0",
|
|
172
|
-
"result": {
|
|
173
|
-
"user": {
|
|
174
|
-
"id": 3,
|
|
175
|
-
"name": "Charlie",
|
|
176
|
-
"email": "charlie@example.com"
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
"id": 3
|
|
180
|
-
}
|
|
126
|
+
-H 'Content-Type: application/json' \
|
|
127
|
+
-H 'Authorization: Bearer demo-token-123' \
|
|
128
|
+
-d '{"jsonrpc":"2.0","method":"tools/list","id":3}'
|
|
181
129
|
```
|
|
182
130
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
{
|
|
186
|
-
"jsonrpc": "2.0",
|
|
187
|
-
"error": {
|
|
188
|
-
"code": -32003,
|
|
189
|
-
"message": "Unauthorized"
|
|
190
|
-
},
|
|
191
|
-
"id": 1
|
|
192
|
-
}
|
|
193
|
-
```
|
|
131
|
+
Each request returns `"jsonrpc":"2.0"` and an empty `resources` or `tools`
|
|
132
|
+
array in the current checkout.
|
|
194
133
|
|
|
195
134
|
## File Structure
|
|
196
135
|
|
|
197
136
|
- `README.md`: This file
|
|
198
137
|
- `app.rb`: Application logic
|
|
199
|
-
- `DemoApp`: Web interface
|
|
200
|
-
- `UserAPI`: MCP handlers
|
|
138
|
+
- `DemoApp`: Web interface and health check
|
|
139
|
+
- `UserAPI`: Intended MCP resource and tool handlers (not registered by the current route-loading path)
|
|
201
140
|
- `config.ru`: Rack configuration (loads Otto, enables MCP)
|
|
202
141
|
- `routes`: Route definitions for web and MCP routes
|
|
203
142
|
|
|
204
|
-
##
|
|
143
|
+
## Routes
|
|
205
144
|
|
|
206
|
-
|
|
207
|
-
Regular HTTP routes for the web interface:
|
|
208
|
-
```
|
|
209
|
-
GET / DemoApp#welcome
|
|
210
|
-
GET /users DemoApp#list_users
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
### MCP Resource Routes
|
|
214
|
-
Read-only resources exposed via MCP:
|
|
215
|
-
```
|
|
216
|
-
MCP /users UserAPI#mcp_list_users
|
|
217
|
-
```
|
|
218
|
-
- Called via: `POST /_mcp` with method `users/list`
|
|
145
|
+
The ordinary web routes in `routes` are active:
|
|
219
146
|
|
|
220
|
-
### MCP Tool Routes
|
|
221
|
-
Executable operations exposed via MCP:
|
|
222
147
|
```
|
|
223
|
-
|
|
148
|
+
GET / DemoApp.index
|
|
149
|
+
GET /health DemoApp.health
|
|
224
150
|
```
|
|
225
|
-
- Called via: `POST /_mcp` with method `create_user`
|
|
226
151
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
| Route Type | Path | Method Name | Handler |
|
|
232
|
-
|-----------|------|-------------|---------|
|
|
233
|
-
| MCP | `/users` | `users/list` | `mcp_list_users` |
|
|
234
|
-
| MCP | `/users/:id` | `users/get` | `mcp_get_user` |
|
|
235
|
-
| TOOL | `/create_user` | `create_user` | `mcp_create_user` |
|
|
236
|
-
| TOOL | `/users/:id/update` | `users/update` | `mcp_update_user` |
|
|
152
|
+
The file also contains `MCP /users UserAPI.mcp_list_users` and
|
|
153
|
+
`TOOL /create_user UserAPI.mcp_create_user`. See [Current Limitations](#current-limitations):
|
|
154
|
+
they are not registered by this checkout's route-loading path.
|
|
237
155
|
|
|
238
156
|
## Next Steps
|
|
239
157
|
|
|
@@ -241,7 +159,3 @@ MCP route paths are converted to method names:
|
|
|
241
159
|
- Integrate with AI systems that support MCP
|
|
242
160
|
- Combine with [Authentication](../authentication_strategies/) for role-based MCP access
|
|
243
161
|
- Explore [Advanced Routes](../advanced_routes/) for more routing patterns
|
|
244
|
-
|
|
245
|
-
## Further Reading
|
|
246
|
-
|
|
247
|
-
- [CLAUDE.md](../../CLAUDE.md#mcp) - Detailed MCP documentation (if available)
|
data/examples/mcp_demo/config.ru
CHANGED
|
@@ -1,265 +1,67 @@
|
|
|
1
1
|
# Otto Security Features Example
|
|
2
2
|
|
|
3
|
-
This example
|
|
3
|
+
This source-checkout example configures CSRF protection, request validation, request-size and parameter limits, trusted proxies, and response security headers. It provides forms for feedback, file-upload metadata, and profile input so you can observe those protections.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Prerequisites
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- Input validation for preventing injection attacks
|
|
9
|
-
- XSS prevention through output escaping
|
|
10
|
-
- Secure file upload handling with filename sanitization
|
|
11
|
-
- Adding security headers (CSP, HSTS, etc.)
|
|
12
|
-
- Request limiting to prevent denial-of-service
|
|
13
|
-
- Trusted proxy configuration for reverse proxies
|
|
14
|
-
- Privacy features (IP masking, user agent anonymization)
|
|
7
|
+
Run this example from an Otto source checkout. Its `config.ru` loads Otto from `../../lib`, rather than from an installed gem.
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
### CSRF Protection
|
|
19
|
-
All POST/PUT/DELETE requests include CSRF tokens in forms:
|
|
20
|
-
```ruby
|
|
21
|
-
<form method="post">
|
|
22
|
-
<input type="hidden" name="_csrf_token" value="<%= @req.csrf_token %>">
|
|
23
|
-
<input type="text" name="message">
|
|
24
|
-
</form>
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### Input Validation
|
|
28
|
-
Server-side validation of user-submitted data:
|
|
29
|
-
- Length limits (max 1000 chars for messages)
|
|
30
|
-
- Character restrictions (no HTML tags)
|
|
31
|
-
- Required field validation
|
|
32
|
-
- Type validation
|
|
33
|
-
|
|
34
|
-
### XSS Prevention
|
|
35
|
-
All output is properly escaped:
|
|
36
|
-
```ruby
|
|
37
|
-
@res.body = "<h1>#{ERB::Util.html_escape(user_input)}</h1>"
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Secure File Uploads
|
|
41
|
-
File uploads are validated and sanitized:
|
|
42
|
-
- File type checking (whitelist approach)
|
|
43
|
-
- Size limits (prevent large uploads)
|
|
44
|
-
- Filename sanitization (remove path traversal)
|
|
45
|
-
- Safe storage location
|
|
46
|
-
|
|
47
|
-
### Security Headers
|
|
48
|
-
Automatic security headers are sent with responses:
|
|
49
|
-
- `Content-Security-Policy` - Prevents inline scripts
|
|
50
|
-
- `Strict-Transport-Security` - Enforces HTTPS
|
|
51
|
-
- `X-Frame-Options` - Prevents clickjacking
|
|
52
|
-
- `X-Content-Type-Options` - Prevents MIME sniffing
|
|
53
|
-
|
|
54
|
-
### Request Limiting
|
|
55
|
-
Configure limits to prevent DOS attacks:
|
|
56
|
-
- Maximum request size
|
|
57
|
-
- Maximum parameter keys
|
|
58
|
-
- Maximum parameter depth
|
|
59
|
-
|
|
60
|
-
### Trusted Proxies
|
|
61
|
-
Configure reverse proxy IPs for X-Forwarded-For headers:
|
|
62
|
-
```ruby
|
|
63
|
-
app.add_trusted_proxy('10.0.0.0/8')
|
|
64
|
-
app.add_trusted_proxy(/^192\.168\./)
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Privacy by Default
|
|
68
|
-
Automatic privacy features:
|
|
69
|
-
- Public IP masking (203.0.113.50 → 203.0.113.0)
|
|
70
|
-
- User agent anonymization (versions stripped)
|
|
71
|
-
- Country-level geo-location only
|
|
72
|
-
- Private/localhost IPs NOT masked by default (configurable via `configure_ip_privacy()`)
|
|
73
|
-
|
|
74
|
-
## How to Run
|
|
75
|
-
|
|
76
|
-
### Using rackup (recommended)
|
|
9
|
+
You need a supported Ruby version and Bundler. `rackup` is a development dependency in the root `Gemfile`, so enable the `development` group before installing dependencies:
|
|
77
10
|
|
|
78
11
|
```sh
|
|
79
|
-
|
|
80
|
-
|
|
12
|
+
# From the repository root
|
|
13
|
+
bundle config set with development
|
|
14
|
+
bundle install
|
|
81
15
|
```
|
|
82
16
|
|
|
83
|
-
|
|
17
|
+
## Run the example
|
|
84
18
|
|
|
85
19
|
```sh
|
|
20
|
+
# From the repository root
|
|
86
21
|
cd examples/security_features
|
|
87
|
-
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Open your browser and navigate to `http://localhost:10770`.
|
|
91
|
-
|
|
92
|
-
## Testing Security Features
|
|
93
|
-
|
|
94
|
-
### XSS Prevention
|
|
95
|
-
|
|
96
|
-
Try entering `<script>alert("XSS")</script>` in form fields:
|
|
97
|
-
- The script tag is rendered as text, not executed
|
|
98
|
-
- You'll see it displayed as literal HTML tags
|
|
99
|
-
- Browser's developer tools show escaped HTML
|
|
100
|
-
|
|
101
|
-
### Input Validation
|
|
102
|
-
|
|
103
|
-
Test validation rules:
|
|
104
|
-
- Submit a message > 1000 characters (fails)
|
|
105
|
-
- Submit special characters like `<>` (fails)
|
|
106
|
-
- Submit valid text (succeeds)
|
|
107
|
-
|
|
108
|
-
### CSRF Protection
|
|
109
|
-
|
|
110
|
-
Examine form submissions:
|
|
111
|
-
- All POST forms include a `_csrf_token` hidden field
|
|
112
|
-
- Each request has a unique token
|
|
113
|
-
- Removing the token causes 403 Forbidden
|
|
114
|
-
- Browser's developer tools show token in form data
|
|
115
|
-
|
|
116
|
-
### File Uploads
|
|
117
|
-
|
|
118
|
-
Test file upload security:
|
|
119
|
-
- Try uploading an executable file (rejected)
|
|
120
|
-
- Try uploading a legitimate image (accepted)
|
|
121
|
-
- Check saved filename (sanitized, safe)
|
|
122
|
-
- Verify file permissions and location
|
|
123
|
-
|
|
124
|
-
### Security Headers
|
|
125
|
-
|
|
126
|
-
Check response headers:
|
|
127
|
-
- Open browser's Network tab in developer tools
|
|
128
|
-
- Click any response to view headers
|
|
129
|
-
- Look for security headers in response
|
|
130
|
-
- Visit `/headers` endpoint to see all headers
|
|
131
|
-
|
|
132
|
-
## Expected Output
|
|
133
|
-
|
|
134
|
-
### Successful Form Submission
|
|
22
|
+
bundle exec rackup config.ru -p 10770
|
|
135
23
|
```
|
|
136
|
-
POST /feedback HTTP/1.1
|
|
137
|
-
Content-Type: application/x-www-form-urlencoded
|
|
138
24
|
|
|
139
|
-
|
|
140
|
-
message=Hello+world
|
|
25
|
+
Open `http://127.0.0.1:10770/`. Keep the server running and use a second terminal for the checks below.
|
|
141
26
|
|
|
142
|
-
|
|
143
|
-
Location: http://localhost:10770/
|
|
144
|
-
Content-Security-Policy: default-src 'self'
|
|
145
|
-
Strict-Transport-Security: max-age=31536000
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Failed CSRF Validation
|
|
149
|
-
```
|
|
150
|
-
HTTP/1.1 403 Forbidden
|
|
151
|
-
Content-Type: text/plain
|
|
27
|
+
## What this example configures
|
|
152
28
|
|
|
153
|
-
CSRF
|
|
154
|
-
```
|
|
29
|
+
`config.ru` enables CSRF protection and request validation, then sets these limits:
|
|
155
30
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Content-Type: text/plain
|
|
31
|
+
- Maximum request size: 5 MiB
|
|
32
|
+
- Maximum parameter nesting depth: 10
|
|
33
|
+
- Maximum parameter keys: 50
|
|
160
34
|
|
|
161
|
-
|
|
162
|
-
```
|
|
35
|
+
It trusts the listed loopback and private-network proxy ranges. Adjust `trusted_proxies` for a real deployment; do not copy this development-oriented list without confirming the proxies that can reach your application.
|
|
163
36
|
|
|
164
|
-
|
|
165
|
-
```
|
|
166
|
-
Content-Security-Policy: default-src 'self'
|
|
167
|
-
Strict-Transport-Security: max-age=31536000
|
|
168
|
-
X-Frame-Options: SAMEORIGIN
|
|
169
|
-
X-Content-Type-Options: nosniff
|
|
170
|
-
Referrer-Policy: strict-origin-when-cross-origin
|
|
171
|
-
```
|
|
37
|
+
Every route receives the configured headers:
|
|
172
38
|
|
|
173
|
-
|
|
39
|
+
- `Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'`
|
|
40
|
+
- `Strict-Transport-Security: max-age=31536000; includeSubDomains`
|
|
41
|
+
- `X-Frame-Options: DENY`
|
|
174
42
|
|
|
175
|
-
|
|
176
|
-
- `app.rb`: Main application logic with security implementations
|
|
177
|
-
- Form validation and escaping
|
|
178
|
-
- File upload handling
|
|
179
|
-
- Header configuration
|
|
180
|
-
- `config.ru`: Rack configuration with security features enabled
|
|
181
|
-
- `routes`: URL routes mapped to SecureApp class methods
|
|
182
|
-
|
|
183
|
-
## Key Configuration
|
|
184
|
-
|
|
185
|
-
In `config.ru`:
|
|
186
|
-
```ruby
|
|
187
|
-
app = Otto.new("./routes")
|
|
188
|
-
|
|
189
|
-
# Enable security features
|
|
190
|
-
app.enable_csrf_protection!
|
|
191
|
-
|
|
192
|
-
# Configure request limits
|
|
193
|
-
app.security_config.request_size_limit = 1.megabyte
|
|
194
|
-
app.security_config.max_parameter_keys = 100
|
|
195
|
-
app.security_config.max_parameter_depth = 5
|
|
196
|
-
|
|
197
|
-
# Add trusted proxies if behind reverse proxy
|
|
198
|
-
app.add_trusted_proxy('10.0.0.0/8')
|
|
199
|
-
|
|
200
|
-
# Security headers
|
|
201
|
-
app.add_security_header('X-Custom-Header', 'value')
|
|
202
|
-
```
|
|
43
|
+
## Verify the protections
|
|
203
44
|
|
|
204
|
-
|
|
45
|
+
Use the diagnostic route to inspect request information and the response headers. A POST without a CSRF token exercises the CSRF rejection path:
|
|
205
46
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
```
|
|
210
|
-
**Result**: Safely displayed as text, not executed
|
|
211
|
-
|
|
212
|
-
### SQL Injection
|
|
213
|
-
```sql
|
|
214
|
-
'; DROP TABLE users; --
|
|
215
|
-
```
|
|
216
|
-
**Result**: Stored as literal text, invalid SQL
|
|
217
|
-
|
|
218
|
-
### Path Traversal
|
|
219
|
-
```
|
|
220
|
-
../../../../../../etc/passwd
|
|
221
|
-
```
|
|
222
|
-
**Result**: Filename sanitized to just `etc-passwd`
|
|
223
|
-
|
|
224
|
-
### Large Request
|
|
225
|
-
```
|
|
226
|
-
POST with 10MB body
|
|
47
|
+
```sh
|
|
48
|
+
curl -i http://127.0.0.1:10770/headers
|
|
49
|
+
curl -i -X POST http://127.0.0.1:10770/feedback -d 'message=test'
|
|
227
50
|
```
|
|
228
|
-
**Result**: Rejected with 413 Payload Too Large
|
|
229
51
|
|
|
230
|
-
|
|
52
|
+
The feedback handler rejects script-like input and limits messages to 1,000 characters; profile fields have their own limits and the email field must match the example's basic format check. The upload form demonstrates filename sanitization and request handling. It displays metadata and does **not** permanently store uploaded files; it does not implement a file-type allowlist.
|
|
231
53
|
|
|
232
|
-
|
|
233
|
-
2. **Input Validation**: Whitelist approach (allow only safe input)
|
|
234
|
-
3. **Output Escaping**: Escape all user-controlled output
|
|
235
|
-
4. **CSRF Tokens**: Unique tokens for each request
|
|
236
|
-
5. **Security Headers**: Prevent common attack vectors
|
|
237
|
-
6. **File Upload Safety**: Validate type and sanitize names
|
|
238
|
-
7. **Request Limiting**: Prevent denial-of-service
|
|
54
|
+
### Current limitation
|
|
239
55
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
```sh
|
|
243
|
-
# Test CSRF protection (will fail without token)
|
|
244
|
-
curl -X POST http://localhost:10770/feedback \
|
|
245
|
-
-d "message=test"
|
|
246
|
-
|
|
247
|
-
# Test with valid CSRF token (get token from form first)
|
|
248
|
-
curl -X POST http://localhost:10770/feedback \
|
|
249
|
-
-d "_csrf_token=<token>" \
|
|
250
|
-
-d "message=test"
|
|
251
|
-
|
|
252
|
-
# Test input validation
|
|
253
|
-
curl -X POST http://localhost:10770/feedback \
|
|
254
|
-
-d "message=$(python -c 'print(\"x\" * 2000)')"
|
|
255
|
-
```
|
|
56
|
+
The home page generates a CSRF token during its first request. Otto freezes configuration at that point, and the generated-secret warning then attempts to modify the frozen security configuration. As a result, `GET /` currently fails with `FrozenError`, so browser-based valid-form verification is unavailable until that example or the initialization sequence is corrected.
|
|
256
57
|
|
|
257
|
-
##
|
|
58
|
+
## Files to inspect
|
|
258
59
|
|
|
259
|
-
-
|
|
260
|
-
-
|
|
60
|
+
- `config.ru` creates the Otto application and sets the security configuration.
|
|
61
|
+
- `routes` defines the form and diagnostic endpoints.
|
|
62
|
+
- `app.rb` renders CSRF form fields and handles validation, escaped output, and uploaded-file metadata.
|
|
261
63
|
|
|
262
|
-
##
|
|
64
|
+
## Next steps
|
|
263
65
|
|
|
264
|
-
- [
|
|
265
|
-
- [
|
|
66
|
+
- See the [authentication strategies example](../authentication_strategies/README.md) for route authentication.
|
|
67
|
+
- Read the [security configuration in the main README](../../README.md#security-features).
|