|
| 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_tenant_column_name = "tenant_id" |
| 24 | +Aikido::Zen.config.idor_excluded_table_names = ["users"] |
| 25 | + |
| 26 | +... |
| 27 | +``` |
| 28 | + |
| 29 | +- `idor_tenant_column_name` — the column name that identifies the tenant in your database tables (e.g. `account_id`, `organization_id`, `team_id`). |
| 30 | +- `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). |
| 31 | + |
| 32 | +### 2. Set the tenant ID per request |
| 33 | + |
| 34 | +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): |
| 35 | + |
| 36 | +```ruby |
| 37 | +Aikido::Zen.set_tenant_id(1) |
| 38 | +``` |
| 39 | + |
| 40 | +> [!IMPORTANT] |
| 41 | +> 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. |
| 42 | +
|
| 43 | +### 3. Bypass for specific queries (optional) |
| 44 | + |
| 45 | +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: |
| 46 | + |
| 47 | +```ruby |
| 48 | +... |
| 49 | + |
| 50 | +# IDOR checks are skipped for queries inside this block |
| 51 | +result = Aikido::Zen.without_idor_protection do |
| 52 | + db.query("SELECT count(*) FROM agents WHERE status = 'running'"); |
| 53 | +end |
| 54 | + |
| 55 | +... |
| 56 | +``` |
| 57 | + |
| 58 | +## Troubleshooting |
| 59 | + |
| 60 | +<details> |
| 61 | +<summary>Missing tenant filter</summary> |
| 62 | + |
| 63 | +``` |
| 64 | +Zen IDOR protection: query on table 'orders' is missing a filter on column 'tenant_id' |
| 65 | +``` |
| 66 | + |
| 67 | +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. |
| 68 | + |
| 69 | +</details> |
| 70 | + |
| 71 | +<details> |
| 72 | +<summary>Wrong tenant ID value</summary> |
| 73 | + |
| 74 | +``` |
| 75 | +Zen IDOR protection: query on table 'orders' filters 'tenant_id' with value '456' but tenant ID is '123' |
| 76 | +``` |
| 77 | + |
| 78 | +This means the query filters on `tenant_id`, but the value doesn't match the tenant ID set via `Aikido::Zen.set_tenant_id`. |
| 79 | + |
| 80 | +</details> |
| 81 | + |
| 82 | +<details> |
| 83 | +<summary>Missing tenant column in INSERT</summary> |
| 84 | + |
| 85 | +``` |
| 86 | +Zen IDOR protection: INSERT on table 'orders' is missing column 'tenant_id' |
| 87 | +``` |
| 88 | + |
| 89 | +This means an `INSERT` statement doesn't include the tenant column. Every INSERT must include the tenant column with the correct tenant ID value. |
| 90 | + |
| 91 | +</details> |
| 92 | + |
| 93 | +<details> |
| 94 | +<summary>Wrong tenant ID in INSERT</summary> |
| 95 | + |
| 96 | +``` |
| 97 | +Zen IDOR protection: INSERT on table 'orders' sets 'tenant_id' to '456' but tenant ID is '123' |
| 98 | +``` |
| 99 | + |
| 100 | +This means the INSERT includes the tenant column, but the value doesn't match the tenant ID set via `Aikido::Zen.set_tenant_id`. |
| 101 | + |
| 102 | +</details> |
| 103 | + |
| 104 | +<details> |
| 105 | +<summary>Missing Aikido::Zen.set_tenant_id call</summary> |
| 106 | + |
| 107 | +``` |
| 108 | +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. |
| 109 | +``` |
| 110 | + |
| 111 | +</details> |
| 112 | + |
| 113 | +## Supported databases |
| 114 | + |
| 115 | +- SQLite (via `sqlite3` Gem) |
| 116 | +- PostgreSQL (via `pg` Gem) |
| 117 | +- MySQL (via `mysql2` and `trilogy` Gems) |
| 118 | + |
| 119 | +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. |
| 120 | + |
| 121 | +## Limitations |
| 122 | + |
| 123 | +## Statements that are always allowed |
| 124 | + |
| 125 | +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: |
| 126 | + |
| 127 | +- DDL — `CREATE TABLE`, `ALTER TABLE`, `DROP TABLE`, ... |
| 128 | +- Session commands — `SET`, `SHOW`, ... |
| 129 | +- Transactions — `BEGIN`, `COMMIT`, `ROLLBACK`, ... |
0 commit comments