Skip to content

[CRITICAL] Cross-database privilege escalation — ON clause bypasses authorization #97

Description

@austin-barrington

Summary

check_authorization validates privileges against the ?db= query parameter, but execution resolves the target database from the statement's ON <db> clause. A non-admin user with write access to any single database can execute destructive operations on databases they have no access to.

Findings covered: #2 (root cause), #6 (bind parameter injection that compounds this)

Root Cause

In application/query_service.rs, check_authorization (lines 185-238) always validates against the db parameter (the ?db= query param from the HTTP request). But execution resolves the effective target from the statement itself:

let target_db = if rp_db.is_empty() { db } else { rp_db.as_str() };

The ON <db> clause in the statement takes precedence over the ?db= param at execution time, but authorization never sees it.

Affected Statements

Statement Auth checks Execution uses
ALTER RETENTION POLICY ... ON victim ?db= ON victim
CREATE RETENTION POLICY ... ON victim ?db= ON victim
DROP RETENTION POLICY ... ON victim ?db= ON victim
CREATE CONTINUOUS QUERY ... ON victim ?db= ON victim
DROP CONTINUOUS QUERY ... ON victim ?db= ON victim
DROP SERIES ... ON victim ?db= ON victim
CREATE MATERIALIZED VIEW ... ON victim ?db= ON victim
DROP MATERIALIZED VIEW ... ON victim ?db= ON victim
SHOW RETENTION POLICIES ON victim ?db= ON victim
SHOW MEASUREMENTS ON victim ?db= ON victim
SHOW TAG KEYS/VALUES ON victim ?db= ON victim
SHOW FIELD KEYS ON victim ?db= ON victim
SHOW SERIES ON victim ?db= ON victim

Not affected: DELETE, DROP MEASUREMENT — these use the request ?db= for execution.

Impact

  • Data destruction: ALTER RETENTION POLICY autogen ON victim DURATION 1h forces deletion of another tenant's data.
  • Schema poisoning: CREATE CONTINUOUS QUERY ... ON victim points CQs at another tenant's database.
  • Information disclosure: SHOW MEASUREMENTS ON victim leaks schema/tag values.
  • Data manipulation: DROP SERIES ... ON victim deletes another tenant's series.

Reproduction

# As a non-admin user with write on "mine":
POST /query?db=mine&q=ALTER%20RETENTION%20POLICY%20autogen%20ON%20victim%20DURATION%201h%20REPLICATION%201
# Returns 200 — authorization passed on "mine", but victim's RP is now 1h

#6 — Bind Parameter Injection (compounds #2)

substitute_bind_params in adapters/http/query.rs (lines 75-92):

  1. Backslash not escaped: Only ' -> '\' escaping. A value ending in \ escapes the closing quote: a\ -> 'a\' -> unterminated string.
  2. Naive replacement: String::replace does substring matching — $host matches inside $hostname.
  3. Order-dependent: HashMap iteration order means overlapping param names produce different results.

Downstream ClickHouse SQL generation is properly escaped (no raw SQL injection), but this is InfluxQL-level injection that compounds the cross-DB bypass in #2.

Recommended Fix

  1. check_authorization must resolve the effective target DB from the statement (the same resolution execution uses) and authorize against that.
  2. For bind params: escape both \ and ', match $name on word boundaries, or bind into the AST post-parse.
  3. Add integration tests asserting 403 for cross-DB statements.

Confidence

High. Traced authz -> execution for every affected statement type.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity vulnerability or hardening

    Type

    No type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions