aikido-zen 1.3.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.
- checksums.yaml +4 -4
- data/.simplecov +1 -1
- data/docs/idor-protection.md +130 -0
- data/docs/invalid-sql-queries.md +9 -0
- data/lib/aikido/zen/attack.rb +5 -3
- data/lib/aikido/zen/config.rb +49 -15
- data/lib/aikido/zen/context/rack_request.rb +28 -4
- data/lib/aikido/zen/context/rails_request.rb +42 -6
- data/lib/aikido/zen/context.rb +5 -0
- data/lib/aikido/zen/helpers.rb +10 -0
- data/lib/aikido/zen/idor/analysis_result.rb +173 -0
- data/lib/aikido/zen/idor/protector.rb +149 -0
- data/lib/aikido/zen/idor.rb +4 -0
- data/lib/aikido/zen/internals.rb +32 -9
- data/lib/aikido/zen/rails_engine.rb +4 -0
- data/lib/aikido/zen/request.rb +15 -5
- data/lib/aikido/zen/scanners/path_traversal_scanner.rb +5 -2
- data/lib/aikido/zen/scanners/shell_injection_scanner.rb +4 -1
- data/lib/aikido/zen/scanners/sql_injection_scanner.rb +32 -31
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +17 -12
- data/lib/aikido/zen/scanners/stored_ssrf_scanner.rb +1 -1
- data/lib/aikido/zen/sink.rb +1 -4
- data/lib/aikido/zen/sinks/mysql2.rb +26 -2
- data/lib/aikido/zen/sinks/pg.rb +42 -10
- data/lib/aikido/zen/sinks/sqlite3.rb +49 -5
- data/lib/aikido/zen/sinks/trilogy.rb +6 -2
- data/lib/aikido/zen/sinks.rb +1 -1
- data/lib/aikido/zen/sql.rb +108 -0
- data/lib/aikido/zen/version.rb +2 -2
- data/lib/aikido/zen.rb +68 -5
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa5de81052f55031065a0e154a9fe929d8928832cbaff2cad5657f0fd262a8ed
|
|
4
|
+
data.tar.gz: 0f7b951bef59f101be88141f284fda6f9786803868e3e54be84fb1f7c5a7909b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36338bd37e39be16311ead1f8caae49dc378f62b8641ee28fb0d7f264f6c3578f1a3db1291e33613e5c4ca443d5b3ba5e64babe827a6a5dcd9cc55fc1e787184
|
|
7
|
+
data.tar.gz: 31cce0645b2d3d092424c41bfccdbcc6166efb9295101074727cdc13b48a1914b2ad3c3db74010996ff2072d1b04231740ea3f90cb4b8b372201af5a4d2ed6fc
|
data/.simplecov
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Due to dependency resolution, on Ruby 2.x we're stuck with a _very_ old
|
|
4
4
|
# SimpleCov version, and it doesn't really give us any benefit to run coverage
|
|
5
5
|
# in separate ruby versions since we don't branch on ruby version in the code.
|
|
6
|
-
return if RUBY_VERSION < "3.0"
|
|
6
|
+
return if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0")
|
|
7
7
|
return if ENV["DISABLE_COVERAGE"] == "true"
|
|
8
8
|
|
|
9
9
|
# Output coverage as LCOV to support CodeCov
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# IDOR Protection
|
|
2
|
+
|
|
3
|
+
IDOR stands for Insecure Direct Object Reference — it's when one account can access another account's data because a query doesn't properly filter by account.
|
|
4
|
+
|
|
5
|
+
If your SaaS has accounts (or organizations, workspaces, teams, ...) and uses a column like `tenant_id` to keep each account's data separate, IDOR protection ensures every SQL query filters on the correct tenant. Zen analyzes queries at runtime and raises an error if a query is missing that filter or uses the wrong tenant ID, catching mistakes like:
|
|
6
|
+
|
|
7
|
+
- A `SELECT` that forgets the tenant filter, letting one account read another's orders
|
|
8
|
+
- An `UPDATE` or `DELETE` without a tenant filter, letting one account modify another's data
|
|
9
|
+
- An `INSERT` that omits the tenant column, creating orphaned or misassigned rows
|
|
10
|
+
|
|
11
|
+
Zen catches these at runtime so they surface during development and testing, not in production. See [IDOR vulnerability explained](https://www.aikido.dev/blog/idor-vulnerability-explained) for more background.
|
|
12
|
+
|
|
13
|
+
> [!IMPORTANT]
|
|
14
|
+
> IDOR protection always raises an `Aikido::Zen::IDOR::Error` on violations regardless of block/detect mode. A missing filter is a developer bug, not an external attack.
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
### 1. Enable IDOR protection at startup
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
Aikido::Zen.config.idor_protection_enabled = true
|
|
24
|
+
Aikido::Zen.config.idor_tenant_column_name = "tenant_id"
|
|
25
|
+
Aikido::Zen.config.idor_excluded_table_names = ["users"]
|
|
26
|
+
|
|
27
|
+
...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `idor_tenant_column_name` — the column name that identifies the tenant in your database tables (e.g. `account_id`, `organization_id`, `team_id`).
|
|
31
|
+
- `idor_excluded_table_names` — tables that Zen should skip IDOR checks for, because rows aren't scoped to a single tenant (e.g. a shared `users` table that stores users across all tenants).
|
|
32
|
+
|
|
33
|
+
### 2. Set the tenant ID per request
|
|
34
|
+
|
|
35
|
+
Every request must have a tenant ID when IDOR protection is enabled. Call `Aikido::Zen.set_tenant_id` early in your request handler (e.g. in middleware after authentication):
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
Aikido::Zen.set_tenant_id(1)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> [!IMPORTANT]
|
|
42
|
+
> If `Aikido::Zen.set_tenant_id` is not called for a request, Zen will raise an `Aikido::Zen::IDOR::Error` when a SQL query is executed.
|
|
43
|
+
|
|
44
|
+
### 3. Bypass for specific queries (optional)
|
|
45
|
+
|
|
46
|
+
Some queries don't need tenant filtering (e.g. aggregations across all tenants for an admin dashboard). Use `Aikido::Zen.without_idor_protection` to bypass the check for a specific block:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
# IDOR checks are skipped for queries inside this block
|
|
52
|
+
result = Aikido::Zen.without_idor_protection do
|
|
53
|
+
db.execute("SELECT count(*) FROM agents WHERE status = 'running'");
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Troubleshooting
|
|
60
|
+
|
|
61
|
+
<details>
|
|
62
|
+
<summary>Missing tenant filter</summary>
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Zen IDOR protection: query on table 'orders' is missing a filter on column 'tenant_id'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This means you have a query like `SELECT * FROM orders WHERE status = 'active'` that doesn't filter on `tenant_id`. The same check applies to `UPDATE` and `DELETE` queries.
|
|
69
|
+
|
|
70
|
+
</details>
|
|
71
|
+
|
|
72
|
+
<details>
|
|
73
|
+
<summary>Wrong tenant ID value</summary>
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Zen IDOR protection: query on table 'orders' filters 'tenant_id' with value '456' but tenant ID is '123'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This means the query filters on `tenant_id`, but the value doesn't match the tenant ID set via `Aikido::Zen.set_tenant_id`.
|
|
80
|
+
|
|
81
|
+
</details>
|
|
82
|
+
|
|
83
|
+
<details>
|
|
84
|
+
<summary>Missing tenant column in INSERT</summary>
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
Zen IDOR protection: INSERT on table 'orders' is missing column 'tenant_id'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This means an `INSERT` statement doesn't include the tenant column. Every INSERT must include the tenant column with the correct tenant ID value.
|
|
91
|
+
|
|
92
|
+
</details>
|
|
93
|
+
|
|
94
|
+
<details>
|
|
95
|
+
<summary>Wrong tenant ID in INSERT</summary>
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
Zen IDOR protection: INSERT on table 'orders' sets 'tenant_id' to '456' but tenant ID is '123'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This means the INSERT includes the tenant column, but the value doesn't match the tenant ID set via `Aikido::Zen.set_tenant_id`.
|
|
102
|
+
|
|
103
|
+
</details>
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary>Missing Aikido::Zen.set_tenant_id call</summary>
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Zen IDOR protection: Aikido::Zen.set_tenant_id was not called for this request. Every request must have a tenant ID when IDOR protection is enabled.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
</details>
|
|
113
|
+
|
|
114
|
+
## Supported databases
|
|
115
|
+
|
|
116
|
+
- SQLite (via `sqlite3` Gem)
|
|
117
|
+
- PostgreSQL (via `pg` Gem)
|
|
118
|
+
- MySQL (via `mysql2` and `trilogy` Gems)
|
|
119
|
+
|
|
120
|
+
Any ORM or query builder that uses these database packages under the hood is supported (e.g. ActiveRecord). ORMs that use their own database engine are not supported unless configured to use a supported driver adapter.
|
|
121
|
+
|
|
122
|
+
## Limitations
|
|
123
|
+
|
|
124
|
+
## Statements that are always allowed
|
|
125
|
+
|
|
126
|
+
Zen only checks statements that read or modify row data (`SELECT`, `INSERT`, `UPDATE`, `DELETE`). The following statement types are also recognized and never trigger an IDOR error:
|
|
127
|
+
|
|
128
|
+
- DDL — `CREATE TABLE`, `ALTER TABLE`, `DROP TABLE`, ...
|
|
129
|
+
- Session commands — `SET`, `SHOW`, ...
|
|
130
|
+
- Transactions — `BEGIN`, `COMMIT`, `ROLLBACK`, ...
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Blocking invalid SQL queries
|
|
2
|
+
|
|
3
|
+
Zen blocks SQL queries that it can't tokenize when they contain user input. This prevents attackers from bypassing SQL injection detection with malformed queries. For example, ClickHouse ignores invalid SQL after `;`, and SQLite runs queries before an unclosed `/*` comment.
|
|
4
|
+
|
|
5
|
+
This is on by default. In blocking mode, these queries are blocked. In detection-only mode, they are reported but still executed.
|
|
6
|
+
|
|
7
|
+
If you see false positives (legitimate queries being blocked), set
|
|
8
|
+
`AIKIDO_BLOCK_INVALID_SQL=false` in your environment, or set
|
|
9
|
+
`Aikido::Zen.config.block_invalid_sql = false`.
|
data/lib/aikido/zen/attack.rb
CHANGED
|
@@ -121,11 +121,12 @@ module Aikido::Zen
|
|
|
121
121
|
attr_reader :input
|
|
122
122
|
attr_reader :dialect
|
|
123
123
|
|
|
124
|
-
def initialize(query:, input:, dialect:, **opts)
|
|
124
|
+
def initialize(query:, input:, dialect:, failed_to_tokenize:, **opts)
|
|
125
125
|
super(**opts)
|
|
126
126
|
@query = query
|
|
127
127
|
@input = input
|
|
128
128
|
@dialect = dialect
|
|
129
|
+
@failed_to_tokenize = failed_to_tokenize
|
|
129
130
|
end
|
|
130
131
|
|
|
131
132
|
def humanized_name
|
|
@@ -139,8 +140,9 @@ module Aikido::Zen
|
|
|
139
140
|
def metadata
|
|
140
141
|
{
|
|
141
142
|
sql: @query,
|
|
142
|
-
dialect: @dialect.name
|
|
143
|
-
|
|
143
|
+
dialect: @dialect.name,
|
|
144
|
+
failedToTokenize: @failed_to_tokenize || nil
|
|
145
|
+
}.compact
|
|
144
146
|
end
|
|
145
147
|
|
|
146
148
|
def exception(*)
|
data/lib/aikido/zen/config.rb
CHANGED
|
@@ -28,6 +28,14 @@ module Aikido::Zen
|
|
|
28
28
|
attr_accessor :blocking_mode
|
|
29
29
|
alias_method :blocking_mode?, :blocking_mode
|
|
30
30
|
|
|
31
|
+
# @return [Boolean] is the agent in debugging mode?
|
|
32
|
+
attr_accessor :debugging
|
|
33
|
+
alias_method :debugging?, :debugging
|
|
34
|
+
|
|
35
|
+
# @return [String] the token obtained when configuring the Firewall in the
|
|
36
|
+
# Aikido interface.
|
|
37
|
+
attr_accessor :api_token
|
|
38
|
+
|
|
31
39
|
# @return [URI] The HTTP host for the Aikido API. Defaults to
|
|
32
40
|
# +https://guard.aikido.dev+.
|
|
33
41
|
attr_reader :api_endpoint
|
|
@@ -39,10 +47,6 @@ module Aikido::Zen
|
|
|
39
47
|
# @return [Hash] HTTP timeouts for communicating with the API.
|
|
40
48
|
attr_reader :api_timeouts
|
|
41
49
|
|
|
42
|
-
# @return [String] the token obtained when configuring the Firewall in the
|
|
43
|
-
# Aikido interface.
|
|
44
|
-
attr_accessor :api_token
|
|
45
|
-
|
|
46
50
|
# @return [Integer] the interval in seconds to poll the runtime API for
|
|
47
51
|
# settings changes. Defaults to evey 60 seconds.
|
|
48
52
|
attr_accessor :polling_interval
|
|
@@ -67,10 +71,6 @@ module Aikido::Zen
|
|
|
67
71
|
# Defaults to `aikido-detached-agent.sock`.
|
|
68
72
|
attr_accessor :detached_agent_socket_path
|
|
69
73
|
|
|
70
|
-
# @return [Boolean] is the agent in debugging mode?
|
|
71
|
-
attr_accessor :debugging
|
|
72
|
-
alias_method :debugging?, :debugging
|
|
73
|
-
|
|
74
74
|
# @return [String] environment specific HTTP header providing the client IP.
|
|
75
75
|
attr_accessor :client_ip_header
|
|
76
76
|
|
|
@@ -165,6 +165,12 @@ module Aikido::Zen
|
|
|
165
165
|
attr_accessor :harden
|
|
166
166
|
alias_method :harden?, :harden
|
|
167
167
|
|
|
168
|
+
# @return [Boolean] whether Aikido Zen should block SQL queries that fail
|
|
169
|
+
# tokenization when user input is present. Defaults to false. Can be set
|
|
170
|
+
# through AIKIDO_BLOCK_INVALID_SQL environment variable.
|
|
171
|
+
attr_accessor :block_invalid_sql
|
|
172
|
+
alias_method :block_invalid_sql?, :block_invalid_sql
|
|
173
|
+
|
|
168
174
|
# @return [Integer] how many suspicious requests are allowed before an
|
|
169
175
|
# attack wave detected event is reported.
|
|
170
176
|
# Defaults to 15 requests.
|
|
@@ -188,19 +194,41 @@ module Aikido::Zen
|
|
|
188
194
|
# Defaults to 15 entries.
|
|
189
195
|
attr_accessor :attack_wave_max_cache_samples
|
|
190
196
|
|
|
197
|
+
# @return [Float, nil] the timeout in seconds for regular expression matching.
|
|
198
|
+
# Applied to selected internal regular expressions to mitigate ReDoS risks.
|
|
199
|
+
# Defaults to 1.0 seconds.
|
|
200
|
+
attr_accessor :redos_regexp_timeout
|
|
201
|
+
|
|
202
|
+
# @return [Boolean] whether the IDOR protection feature is enabled.
|
|
203
|
+
# Defaults to false.
|
|
204
|
+
attr_accessor :idor_protection_enabled
|
|
205
|
+
alias_method :idor_protection_enabled?, :idor_protection_enabled
|
|
206
|
+
|
|
207
|
+
# @return [String] the tenant column name for IDOR protection.
|
|
208
|
+
# Defaults to nil.
|
|
209
|
+
attr_accessor :idor_tenant_column_name
|
|
210
|
+
|
|
211
|
+
# @return [Array<String>] the table names to exclude for IDOR protection.
|
|
212
|
+
# Defaults to [].
|
|
213
|
+
attr_accessor :idor_excluded_table_names
|
|
214
|
+
|
|
215
|
+
# @return [Integer] the maximum number of entries in the LRU cache.
|
|
216
|
+
# Defaults to 1000 entries.
|
|
217
|
+
attr_accessor :idor_max_cache_entries
|
|
218
|
+
|
|
191
219
|
def initialize
|
|
192
220
|
self.insert_middleware_after = ::ActionDispatch::RemoteIp
|
|
193
221
|
self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLE", false)) || read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
|
|
194
222
|
self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK", false))
|
|
195
|
-
self.
|
|
223
|
+
self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
|
|
224
|
+
self.api_token = ENV.fetch("AIKIDO_TOKEN", nil)
|
|
196
225
|
self.api_endpoint = ENV.fetch("AIKIDO_ENDPOINT", DEFAULT_AIKIDO_ENDPOINT)
|
|
197
226
|
self.realtime_endpoint = ENV.fetch("AIKIDO_REALTIME_ENDPOINT", DEFAULT_RUNTIME_BASE_URL)
|
|
198
|
-
self.
|
|
227
|
+
self.api_timeouts = 10
|
|
199
228
|
self.polling_interval = 60 # 1 min
|
|
200
229
|
self.initial_heartbeat_delays = [30, 60 * 2] # 30 sec, 2 min
|
|
201
230
|
self.json_encoder = DEFAULT_JSON_ENCODER
|
|
202
231
|
self.json_decoder = DEFAULT_JSON_DECODER
|
|
203
|
-
self.debugging = read_boolean_from_env(ENV.fetch("AIKIDO_DEBUG", false))
|
|
204
232
|
self.logger = Logger.new($stdout, progname: "aikido", level: debugging ? Logger::DEBUG : Logger::INFO)
|
|
205
233
|
self.detached_agent_socket_path = ENV.fetch("AIKIDO_DETACHED_AGENT_SOCKET_PATH", DEFAULT_DETACHED_AGENT_SOCKET_PATH)
|
|
206
234
|
self.client_ip_header = ENV.fetch("AIKIDO_CLIENT_IP_HEADER", nil)
|
|
@@ -208,25 +236,31 @@ module Aikido::Zen
|
|
|
208
236
|
self.max_compressed_stats = 100
|
|
209
237
|
self.max_outbound_connections = 200
|
|
210
238
|
self.max_users_tracked = 1000
|
|
211
|
-
self.request_builder = Aikido::Zen::Context::RACK_REQUEST_BUILDER
|
|
212
239
|
self.blocked_responder = DEFAULT_BLOCKED_RESPONDER
|
|
213
240
|
self.rate_limited_responder = DEFAULT_RATE_LIMITED_RESPONDER
|
|
214
241
|
self.rate_limiting_discriminator = DEFAULT_RATE_LIMITING_DISCRIMINATOR
|
|
215
|
-
self.server_rate_limit_deadline = 30 * 60 # 30 min
|
|
216
|
-
self.client_rate_limit_period = 60 * 60 # 1 hour
|
|
217
|
-
self.client_rate_limit_max_events = 100
|
|
218
242
|
self.collect_api_schema = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", true))
|
|
219
243
|
self.api_schema_max_samples = Integer(ENV.fetch("AIKIDO_MAX_API_DISCOVERY_SAMPLES", 10))
|
|
220
244
|
self.api_schema_collection_max_depth = 20
|
|
221
245
|
self.api_schema_collection_max_properties = 20
|
|
246
|
+
self.request_builder = Aikido::Zen::Context::RACK_REQUEST_BUILDER
|
|
247
|
+
self.client_rate_limit_period = 60 * 60 # 1 hour
|
|
248
|
+
self.client_rate_limit_max_events = 100
|
|
249
|
+
self.server_rate_limit_deadline = 30 * 60 # 30 min
|
|
222
250
|
self.stored_ssrf = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_STORED_SSRF", true))
|
|
223
251
|
self.imds_allowed_hosts = ["metadata.google.internal", "metadata.goog"]
|
|
224
252
|
self.harden = read_boolean_from_env(ENV.fetch("AIKIDO_HARDEN", true))
|
|
253
|
+
self.block_invalid_sql = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCK_INVALID_SQL", false))
|
|
225
254
|
self.attack_wave_threshold = 15
|
|
226
255
|
self.attack_wave_min_time_between_requests = 60 * 1000 # 1 min (ms)
|
|
227
256
|
self.attack_wave_min_time_between_events = 20 * 60 * 1000 # 20 min (ms)
|
|
228
257
|
self.attack_wave_max_cache_entries = 10_000
|
|
229
258
|
self.attack_wave_max_cache_samples = 15
|
|
259
|
+
self.redos_regexp_timeout = 1.0
|
|
260
|
+
self.idor_protection_enabled = false
|
|
261
|
+
self.idor_tenant_column_name = nil
|
|
262
|
+
self.idor_excluded_table_names = []
|
|
263
|
+
self.idor_max_cache_entries = 1000
|
|
230
264
|
end
|
|
231
265
|
|
|
232
266
|
# Set the base URL for API requests.
|
|
@@ -14,12 +14,36 @@ module Aikido::Zen
|
|
|
14
14
|
request = Aikido::Zen::Request.new(delegate, framework: "rack", router: router)
|
|
15
15
|
|
|
16
16
|
Context.new(request) do |req|
|
|
17
|
+
query = begin
|
|
18
|
+
req.GET
|
|
19
|
+
rescue
|
|
20
|
+
{}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
body = begin
|
|
24
|
+
req.POST
|
|
25
|
+
rescue
|
|
26
|
+
{}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
header = begin
|
|
30
|
+
req.normalized_headers
|
|
31
|
+
rescue
|
|
32
|
+
{}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
cookie = begin
|
|
36
|
+
req.cookies
|
|
37
|
+
rescue
|
|
38
|
+
{}
|
|
39
|
+
end
|
|
40
|
+
|
|
17
41
|
{
|
|
18
|
-
query:
|
|
19
|
-
body:
|
|
42
|
+
query: query,
|
|
43
|
+
body: body,
|
|
20
44
|
route: {},
|
|
21
|
-
header:
|
|
22
|
-
cookie:
|
|
45
|
+
header: header,
|
|
46
|
+
cookie: cookie,
|
|
23
47
|
subdomain: []
|
|
24
48
|
}
|
|
25
49
|
end
|
|
@@ -34,13 +34,49 @@ module Aikido::Zen
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
Context.new(request) do |req|
|
|
37
|
+
query = begin
|
|
38
|
+
req.query_parameters
|
|
39
|
+
rescue
|
|
40
|
+
{}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
body = begin
|
|
44
|
+
req.request_parameters
|
|
45
|
+
rescue
|
|
46
|
+
{}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
route = begin
|
|
50
|
+
req.path_parameters
|
|
51
|
+
rescue
|
|
52
|
+
{}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
header = begin
|
|
56
|
+
req.normalized_headers
|
|
57
|
+
rescue
|
|
58
|
+
{}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
cookie = begin
|
|
62
|
+
decrypt_cookies.call(req)
|
|
63
|
+
rescue
|
|
64
|
+
{}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
subdomain = begin
|
|
68
|
+
req.subdomains
|
|
69
|
+
rescue
|
|
70
|
+
[]
|
|
71
|
+
end
|
|
72
|
+
|
|
37
73
|
{
|
|
38
|
-
query:
|
|
39
|
-
body:
|
|
40
|
-
route:
|
|
41
|
-
header:
|
|
42
|
-
cookie:
|
|
43
|
-
subdomain:
|
|
74
|
+
query: query,
|
|
75
|
+
body: body,
|
|
76
|
+
route: route,
|
|
77
|
+
header: header,
|
|
78
|
+
cookie: cookie,
|
|
79
|
+
subdomain: subdomain
|
|
44
80
|
}
|
|
45
81
|
end
|
|
46
82
|
end
|
data/lib/aikido/zen/context.rb
CHANGED
|
@@ -30,6 +30,10 @@ module Aikido::Zen
|
|
|
30
30
|
attr_accessor :protection_disabled
|
|
31
31
|
alias_method :protection_disabled?, :protection_disabled
|
|
32
32
|
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
attr_accessor :idor_protection_enabled
|
|
35
|
+
alias_method :idor_protection_enabled?, :idor_protection_enabled
|
|
36
|
+
|
|
33
37
|
# @param request [Rack::Request] a Request object that implements the
|
|
34
38
|
# Rack::Request API, to which we will delegate behavior.
|
|
35
39
|
# @param settings [Aikido::Zen::RuntimeSettings]
|
|
@@ -45,6 +49,7 @@ module Aikido::Zen
|
|
|
45
49
|
@metadata = {}
|
|
46
50
|
@scanning = false
|
|
47
51
|
@protection_disabled = false
|
|
52
|
+
@idor_protection_enabled = false
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
# Fetch some metadata stored in the Context.
|
data/lib/aikido/zen/helpers.rb
CHANGED
|
@@ -19,6 +19,16 @@ module Aikido
|
|
|
19
19
|
normalized_path.chomp!("/") unless normalized_path == "/"
|
|
20
20
|
normalized_path
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
# Returns a copy of the regexp with the timeout set if timeout is supported.
|
|
24
|
+
#
|
|
25
|
+
# @param regexp [Regexp] the regexp
|
|
26
|
+
# @return [Regexp] the regexp with timeout set
|
|
27
|
+
def self.regexp_with_timeout(regexp, timeout: Aikido::Zen.config.redos_regexp_timeout)
|
|
28
|
+
return regexp if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2")
|
|
29
|
+
|
|
30
|
+
Regexp.new(regexp.source, regexp.options, timeout: timeout)
|
|
31
|
+
end
|
|
22
32
|
end
|
|
23
33
|
end
|
|
24
34
|
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aikido::Zen
|
|
4
|
+
module IDOR
|
|
5
|
+
class Table
|
|
6
|
+
def self.from_json(data)
|
|
7
|
+
new(
|
|
8
|
+
name: data["name"],
|
|
9
|
+
alt_name: data["alias"]
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @param name [String]
|
|
14
|
+
# @param alt_name [String, nil]
|
|
15
|
+
def initialize(name:, alt_name: nil)
|
|
16
|
+
@name = name
|
|
17
|
+
@alt_name = alt_name
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [String]
|
|
21
|
+
attr_accessor :name
|
|
22
|
+
# @return [String, nil]
|
|
23
|
+
attr_accessor :alt_name
|
|
24
|
+
|
|
25
|
+
def ==(other)
|
|
26
|
+
other.is_a?(self.class) &&
|
|
27
|
+
other.name == name &&
|
|
28
|
+
other.alt_name == alt_name
|
|
29
|
+
end
|
|
30
|
+
alias_method :eql?, :==
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class FilterColumn
|
|
34
|
+
def self.from_json(data)
|
|
35
|
+
new(
|
|
36
|
+
table_qualifier: data["table"],
|
|
37
|
+
name: data["column"],
|
|
38
|
+
value: data["value"],
|
|
39
|
+
is_placeholder: data["is_placeholder"],
|
|
40
|
+
placeholder_number: data["placeholder_number"]
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param table_qualifier [String, nil]
|
|
45
|
+
# @param name [String]
|
|
46
|
+
# @param value [String]
|
|
47
|
+
# @param is_placeholder [Boolean]
|
|
48
|
+
# @param placeholder_number [Integer, nil]
|
|
49
|
+
def initialize(table_qualifier:, name:, value:, is_placeholder:, placeholder_number: nil)
|
|
50
|
+
@table_qualifier = table_qualifier
|
|
51
|
+
@name = name
|
|
52
|
+
@value = value
|
|
53
|
+
@is_placeholder = is_placeholder
|
|
54
|
+
@placeholder_number = placeholder_number
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [String, nil]
|
|
58
|
+
attr_accessor :table_qualifier
|
|
59
|
+
|
|
60
|
+
# @return [String]
|
|
61
|
+
attr_accessor :name
|
|
62
|
+
|
|
63
|
+
# @return [String]
|
|
64
|
+
attr_accessor :value
|
|
65
|
+
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
attr_accessor :is_placeholder
|
|
68
|
+
|
|
69
|
+
# @return [Integer, nil]
|
|
70
|
+
attr_accessor :placeholder_number
|
|
71
|
+
|
|
72
|
+
def ==(other)
|
|
73
|
+
other.is_a?(self.class) &&
|
|
74
|
+
other.table_qualifier == table_qualifier &&
|
|
75
|
+
other.name == name &&
|
|
76
|
+
other.value == value &&
|
|
77
|
+
other.is_placeholder == is_placeholder &&
|
|
78
|
+
other.placeholder_number == placeholder_number
|
|
79
|
+
end
|
|
80
|
+
alias_method :eql?, :==
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class InsertColumn
|
|
84
|
+
def self.from_json(data)
|
|
85
|
+
new(
|
|
86
|
+
name: data["column"],
|
|
87
|
+
value: data["value"],
|
|
88
|
+
is_placeholder: data["is_placeholder"],
|
|
89
|
+
placeholder_number: data["placeholder_number"]
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @param name [String]
|
|
94
|
+
# @param value [String]
|
|
95
|
+
# @param is_placeholder [Boolean]
|
|
96
|
+
# @param placeholder_number [Integer, nil]
|
|
97
|
+
def initialize(name:, value:, is_placeholder:, placeholder_number: nil)
|
|
98
|
+
@name = name
|
|
99
|
+
@value = value
|
|
100
|
+
@is_placeholder = is_placeholder
|
|
101
|
+
@placeholder_number = placeholder_number
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [String]
|
|
105
|
+
attr_accessor :name
|
|
106
|
+
|
|
107
|
+
# @return [String]
|
|
108
|
+
attr_accessor :value
|
|
109
|
+
|
|
110
|
+
# @return [Boolean]
|
|
111
|
+
attr_accessor :is_placeholder
|
|
112
|
+
|
|
113
|
+
# @return [Integer, nil]
|
|
114
|
+
attr_accessor :placeholder_number
|
|
115
|
+
|
|
116
|
+
def ==(other)
|
|
117
|
+
other.is_a?(self.class) &&
|
|
118
|
+
other.name == name &&
|
|
119
|
+
other.value == value &&
|
|
120
|
+
other.is_placeholder == is_placeholder &&
|
|
121
|
+
other.placeholder_number == placeholder_number
|
|
122
|
+
end
|
|
123
|
+
alias_method :eql?, :==
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
class SQLQueryResult
|
|
127
|
+
def self.from_json(data)
|
|
128
|
+
new(
|
|
129
|
+
kind: data["kind"].to_sym,
|
|
130
|
+
tables: data["tables"].map { |value| Table.from_json(value) },
|
|
131
|
+
filter_columns: data["filters"].map { |value| FilterColumn.from_json(value) },
|
|
132
|
+
insert_columns: data["insert_columns"]&.map do |values|
|
|
133
|
+
values.map { |value| InsertColumn.from_json(value) }
|
|
134
|
+
end
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @param kind [:select, :insert, :update, :delete]
|
|
139
|
+
# @param tables [Array<Aikido::Zen::IDOR::Table>]
|
|
140
|
+
# @param filter_columns [Array<Aikido::Zen::IDOR::FilterColumn>]
|
|
141
|
+
# @param insert_columns [Array<Array<Aikido::Zen::IDOR::InsertColumn>>, nil]
|
|
142
|
+
def initialize(kind:, tables:, filter_columns:, insert_columns: nil)
|
|
143
|
+
raise ArgumentError, "kind must be one of :select, :insert, :update, or :delete" unless [:select, :insert, :update, :delete].include?(kind)
|
|
144
|
+
|
|
145
|
+
@kind = kind
|
|
146
|
+
@tables = tables
|
|
147
|
+
@filter_columns = filter_columns
|
|
148
|
+
@insert_columns = insert_columns
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @return [:select, :insert, :update, :delete]
|
|
152
|
+
attr_accessor :kind
|
|
153
|
+
|
|
154
|
+
# @return [Array<Aikido::Zen::IDOR::Table>]
|
|
155
|
+
attr_accessor :tables
|
|
156
|
+
|
|
157
|
+
# @return [Array<Aikido::Zen::IDOR::FilterColumn>]
|
|
158
|
+
attr_accessor :filter_columns
|
|
159
|
+
|
|
160
|
+
# @return [Array<Array<Aikido::Zen::IDOR::InsertColumn>>, nil]
|
|
161
|
+
attr_accessor :insert_columns
|
|
162
|
+
|
|
163
|
+
def ==(other)
|
|
164
|
+
other.is_a?(self.class) &&
|
|
165
|
+
other.kind == kind &&
|
|
166
|
+
other.tables == tables &&
|
|
167
|
+
other.filter_columns == filter_columns &&
|
|
168
|
+
other.insert_columns == insert_columns
|
|
169
|
+
end
|
|
170
|
+
alias_method :eql?, :==
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|