Skip to content

fix: escape id path parameter before interpolating into PostgREST filters - #19

Merged
sonnymay merged 5 commits into
mainfrom
fix/id-path-param-filter-injection
Sep 10, 2026
Merged

sonnymay merged 5 commits into
mainfrom
fix/id-path-param-filter-injection

Conversation

@sonnymay

@sonnymay sonnymay commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Summary

Testing

  • pytest -q — 43 passed
  • ruff check ., ruff format --check ., mypy .., pre-commit run --all-files — all clean
  • Six targeted cases cover db_patch, db_delete, each {id} GET route, and the PUT status lookup.

Notes

  • Real UUID values pass through unchanged.
  • The template's own &select= and &order= remain structural.

🤖 Generated with Claude Code

sonnymay and others added 2 commits September 10, 2026 16:11
search_tickets (GET /tickets/search) and filter_tickets (GET /tickets/filter)
interpolated user-supplied values (q, status, priority, assigned_user_id)
directly into PostgREST filter strings. Comma and parentheses are structural
in PostgREST or() syntax and nothing encoded & or =, so a value such as
"x),status.eq.Closed&limit=1" could close the or() group, append extra
conditions, and inject additional query parameters.

Add database.escape_filter_value, which backslash-escapes PostgREST's
reserved characters (\ , ( )) and then percent-encodes the result with
safe="", and route every interpolated value through it. The surrounding
template (wildcards, operators, &order=) is left untouched.

Tests cover plain-text passthrough, that reserved characters never survive
unescaped, the exact escape-then-encode output, and end-to-end injection
attempts against both endpoints.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6wR9fK3YEMMKDwEAwr5Ju
…ters

db_patch, db_delete, and the {id}-based ticket routes (GET /tickets/{id},
GET /tickets/{id}/notes, GET /tickets/{id}/history, and the status-history
lookup in PUT /tickets/{id}) interpolated the raw id path parameter into
PostgREST filter strings such as "id=eq.{id}". FastAPI decodes the path
segment before handing it over, so a crafted id like "t1&status=eq.Closed"
could append additional filters or query parameters to the Supabase request.

Route every id through database.escape_filter_value (introduced for the
search/filter endpoints) so the value is backslash-escaped for PostgREST's
reserved characters and percent-encoded before it reaches the query string.

Tests cover db_patch/db_delete directly and each affected route end-to-end.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6wR9fK3YEMMKDwEAwr5Ju
@vercel

vercel Bot commented Sep 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
supportops Ready Ready Preview Sep 10, 2026 6:07pm UTC

@sonnymay sonnymay left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation review: PASS at 80bb3ae. Every listed id filter is escaped exactly once, UUIDs remain unchanged, the select/order template suffixes remain structural, and the combined 43-test plus lint, format, type-check, and pre-commit suite passes.

@sonnymay
sonnymay deleted the branch main September 10, 2026 18:05
@sonnymay sonnymay closed this Sep 10, 2026
@sonnymay sonnymay reopened this Sep 10, 2026
@sonnymay
sonnymay changed the base branch from fix/postgrest-filter-injection to main September 10, 2026 18:06
…ter-injection

# Conflicts:
#	CHANGELOG.md
#	backend/test_database.py
#	backend/test_main.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant