Skip to content

SQL injection via filter parameter in eql_v2.sort_compare() #181

Description

@coderdan

Summary

eql_v2.sort_compare(id_column, val_column, tbl, direction, filter) (in src/operators/sort.sql:475) concatenates the raw filter text into a dynamically-built query at line 498:

```sql
IF filter IS NOT NULL THEN
query := query || ' WHERE ' || filter;
END IF;
```

The constructed query is then handed to eql_v2.order_by_compare(query, direction) (src/operators/order_by.sql), which executes it via EXECUTE format(... %s, query). Two layers of dynamic SQL, with the user-supplied filter interpolated raw at the inner layer.

The function is not SECURITY DEFINER, so an attacker calling it directly only escalates within their own session. The risk is when an application forwards untrusted input (e.g. from a request parameter) into filter — which is exactly what its @warning says not to do, but it's the kind of footgun worth removing.

Reproducer

```sql
-- Anything past the WHERE clause executes as written
SELECT * FROM eql_v2.sort_compare('id', 'e', 'users', 'ASC',
'1=1; CREATE TABLE pwned(x int)--');
```

Mitigation options

In rough order of effort:

  1. Remove the convenience overload entirely and require callers to pre-build the rows array and use the sort_compare(bigint[], eql_v2_encrypted[], text) overload, which doesn't accept arbitrary SQL. Forces callers to explicitly opt into dynamic SQL.

  2. Restrict filter to a structured, non-textual form — e.g. accept a jsonb filter spec describing column/operator/value tuples, then build the WHERE clause from it using format('%I %s %L', ...). More work but eliminates the injection vector.

  3. Move the body into a SECURITY DEFINER function with a strict allowlist for what filter can contain (regex check for safe characters/keywords). Cheaper but still relies on a denylist that's hard to get right.

  4. REVOKE EXECUTE FROM PUBLIC on the convenience overload and document it as internal/server-side-only. Doesn't fix the vulnerability but limits who can reach it. This is the minimum-risk shipping option and could go in alongside any of (1)-(3).

Notes

Related

Metadata

Metadata

Assignees

Labels

GitHubbugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions