Skip to content

fix: filter transactions by user_id to prevent data exposure#31

Open
memosr wants to merge 2 commits into
circlefin:masterfrom
memosr:fix/transactions-missing-user-id-filter
Open

fix: filter transactions by user_id to prevent data exposure#31
memosr wants to merge 2 commits into
circlefin:masterfrom
memosr:fix/transactions-missing-user-id-filter

Conversation

@memosr
Copy link
Copy Markdown

@memosr memosr commented Apr 11, 2026

Problem

The GET /api/transactions endpoint authenticates the user but fetches all transactions with transaction_type = 'USER' — without filtering by the authenticated user's ID.

This means any logged-in user can see every other user's transaction history.

Fix

Added .eq("user_id", user.id) filter to the Supabase query so each user only sees their own transactions.

- .eq("transaction_type", "USER")
+ .eq("transaction_type", "USER")
+ .eq("user_id", user.id)

Impact

  • Security: Prevents unauthorized access to other users' transaction data
  • Privacy: Each user now only sees their own transaction history
  • Risk: Low — additive filter, no breaking changes

@Cassxbt
Copy link
Copy Markdown

Cassxbt commented May 21, 2026

Reviewed against PR head commit e9d86b5450a71f66e55d2f7104cf144c0289a81c.

The list endpoint now has the right owner filter, but the same trust boundary still needs to be applied to the other user-facing transaction reads.

This PR adds .eq("user_id", user.id) to GET /api/transactions, which fixes the list query. The remaining gap is that current migrations no longer make RLS a sufficient owner boundary for authenticated transaction reads: 20251124000000_fix_admin_transaction_rls_policy.sql adds a broad authenticated read policy, and 20251124000002_clean_up_duplicate_rls_policies.sql drops the older owner-only read policy.

That leaves these paths still able to read by public/shared identifiers without an explicit owner filter:

  • app/api/transactions/[id]/route.ts fetches by .eq("id", id) only, then returns userId, walletId, metadata, and idempotencyKey.
  • app/dashboard/[txHash]/page.tsx fetches by .eq("tx_hash", txHash) only.
  • The duplicate insert fallback in POST /api/transactions uses supabaseAdminClient, filters only by .eq("idempotency_key", idempotencyKey), and returns the existing transaction.

I’d extend the PR so every authenticated user-facing transaction lookup also constrains by the current user, e.g.:

.eq("id", id)
.eq("user_id", user.id)

and for the duplicate fallback:

.eq("idempotency_key", idempotencyKey)
.eq("user_id", user.id)

That keeps the current fix, but closes the direct lookup/idempotency paths instead of only the dashboard list.

@memosr
Copy link
Copy Markdown
Author

memosr commented May 21, 2026

Thanks for the thorough analysis, @Cassxbt! You're right that with the new RLS policies the owner boundary needs to be explicit at every authenticated read path.

All three gaps closed in 9c7cec6:

  • app/api/transactions/[id]/route.ts (GET) — added .eq("user_id", user.id) after .eq("id", id). Closes the UUID-guessing gap.
  • app/api/transactions/route.ts (POST idempotency fallback) — added .eq("user_id", user.id) to the admin-client duplicate lookup. Prevents the chainId:txHash idempotency key (both public values) from leaking another user's record.
  • app/dashboard/[txHash]/page.tsx — added .eq("user_id", data.user.id) after .eq("tx_hash", txHash). Prevents viewing another user's receipt via direct URL navigation.

Each path was already authenticated, so the only change was the additional owner filter.

@Cassxbt
Copy link
Copy Markdown

Cassxbt commented May 25, 2026

Re-checked 9c7cec6. The explicit owner filter is now applied to all three paths I flagged, including the admin-client idempotency fallback. This resolves my concern.

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.

2 participants