Skip to content

Harden policy expression handling in policy create/update endpoints #167

@bhudevbhanpuriya

Description

@bhudevbhanpuriya

Description

The policy create/update endpoints currently accept a policy condition as text and place it into CREATE POLICY / ALTER POLICY SQL.

Even though the code blocks obvious dangerous tokens like semicolons and comments, the condition is still raw SQL supplied by the request body. That makes this a brittle security boundary: a malformed or cleverly crafted expression may try to break out of USING (...) or WITH CHECK (...), causing syntax abuse or possible policy bypass behavior.

Affected Area

api/app/v1/endpoints/update/policy.py

The update endpoint builds SQL like:

ALTER POLICY ... USING ({policy_expression})
ALTER POLICY ... WITH CHECK ({policy_expression})

A similar pattern also exists in custom policy creation:

api/app/v1/endpoints/create/policy.py

Why This Matters

Policy expressions are security-sensitive because they define which rows a user can read or modify.

Validation by blacklist is not enough here. PostgreSQL has to parse the expression as SQL, so if we accept arbitrary SQL text from the API, we are trusting request input inside a privileged DDL statement.

Suggested Fix

Replace raw policy-expression interpolation with a constrained expression renderer/parser.

Instead of accepting arbitrary SQL, support a small safe grammar, for example:

true
network = 'IDROLOGIA'
"id" IN (1, 2, 3)
"name" IS NOT NULL
status = 'active' AND public = true
The renderer should:

Parse the input as a limited boolean expression

  • Quote identifiers itself
  • Quote string literals itself
  • Reject extra SQL structure such as nested SELECT, semicolons, comments, or attempts to close parentheses early
  • Be shared by both create and update policy flows

Acceptance Criteria

  • update_policy no longer inserts raw request policy text directly into SQL.
  • Custom create_policies uses the same safe policy-expression handling.
  • Valid simple policy expressions still work.
  • Expressions such as these are rejected before SQL execution:
true) WITH CHECK (false
network = 'x'; DROP TABLE users
exists (select 1)
  • Regression tests cover both accepted and rejected policy expressions.
  • Notes

PostgreSQL does not allow binding an entire policy predicate as a normal query parameter like $1, because the predicate is SQL syntax, not a data value. So the safer approach is to parse a limited input language and render trusted SQL from it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions