Skip to content

Add Row Level Security policy to profiles table - #1876

Merged
durdana3105 merged 1 commit into
durdana3105:mainfrom
anshul23102:fix/issue-1870-profiles-rls
Jul 31, 2026
Merged

Add Row Level Security policy to profiles table#1876
durdana3105 merged 1 commit into
durdana3105:mainfrom
anshul23102:fix/issue-1870-profiles-rls

Conversation

@anshul23102

@anshul23102 anshul23102 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #1870

Summary

Implements Row Level Security (RLS) policies on the profiles table to prevent unauthorized access to user emails and profile metadata. This closes a critical data exposure vulnerability where any visitor using the exposed VITE_SUPABASE_ANON_KEY could retrieve all user records.

Security Fix

Problem

The profiles table lacked RLS policies, allowing unrestricted read access to all user records via the public anon API key:

curl 'https://<project>.supabase.co/rest/v1/profiles?select=id,email' \\n  -H 'apikey: <anon_key>'
# Returns all user emails and profile data

Solution

Added comprehensive RLS policies:

  • SELECT: Users can only read their own profile (auth.uid() = id)
  • UPDATE: Users can only modify their own profile
  • INSERT: Users can only create their own profile entry
  • Service Role: Backend retains full access for internal operations
  • Unauthenticated: Completely blocked from accessing any profiles

Implementation

New migration: supabase/migrations/20260729000000_add_profiles_rls_policy.sql

Policies implemented:

  1. users_read_own_profile - SELECT USING (auth.uid() = id)
  2. users_update_own_profile - UPDATE with auth.uid() check
  3. users_create_own_profile - INSERT with auth.uid() check
  4. service_role_read_all_profiles - Backend access
  5. service_role_update_profiles - Backend modification

Impact

  • ✅ Prevents mass PII harvesting
  • ✅ Blocks unauthorized email exposure
  • ✅ Maintains service role functionality for backend operations
  • ✅ Follows Supabase security best practices

Testing

  • Verify RLS is enabled on profiles table
  • Confirm unauthenticated access is blocked
  • Test that users can only access their own profile
  • Confirm service role still has full access

Summary by CodeRabbit

  • Security
    • Improved profile data protection with access controls that limit users to viewing, creating, and updating only their own profile information.
    • Maintained authorized internal access for required service operations.

@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

@anshul23102 is attempting to deploy a commit to the durdana3105's projects Team on Vercel.

A member of the Team first needs to authorize it.

@anshul23102

Copy link
Copy Markdown
Contributor Author

Thank you for reviewing this security fix!

This PR implements critical Row Level Security policies on the profiles table to prevent unauthorized access to user emails and profile metadata.

Security Impact:

  • Blocks PII exposure via public API key
  • Restricts profile access to authenticated users only
  • Maintains backend service role access for internal operations

When ready, please review the changes and consider adding these labels:

  • gssoc-approved - GSSoC 2026 contribution
  • security - security enhancement
  • database - database/migration related
  • high priority - critical vulnerability fix

Thank you!

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@anshul23102, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 53f83021-46ca-46bf-bc68-4ba17bb93d28

📥 Commits

Reviewing files that changed from the base of the PR and between f5ae513 and 5d6213a.

📒 Files selected for processing (1)
  • supabase/migrations/20260729000000_add_profiles_rls_policy.sql
📝 Walkthrough

Walkthrough

Adds a Supabase migration that enables RLS on profiles and restricts user operations to matching profile IDs, while permitting service-role profile access.

Changes

Profiles RLS enforcement

Layer / File(s) Summary
Add profiles RLS policies
supabase/migrations/20260729000000_add_profiles_rls_policy.sql
Enables RLS and adds owner-scoped SELECT, UPDATE, and INSERT policies, alongside service-role SELECT and UPDATE policies.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: type:bug, quality:clean

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding RLS policies to the profiles table.
Linked Issues check ✅ Passed The migration enables RLS and restricts profile read/update access to the row owner, satisfying issue #1870.
Out of Scope Changes check ✅ Passed The extra insert and service-role policies align with the stated objectives and do not appear unrelated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Drop 'Public profiles are viewable by everyone' policy
Replace with secure 'profiles_select_own' policy
Restrict reads to authenticated user's own profile (auth.uid() = id)
Prevents unauthorized email and profile metadata exposure via anon API key
@anshul23102
anshul23102 force-pushed the fix/issue-1870-profiles-rls branch from f5ae513 to 5d6213a Compare July 29, 2026 12:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
supabase/migrations/20260729000000_add_profiles_rls_policy.sql (2)

34-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate INSERT policy; also no DROP POLICY IF EXISTS guard, unlike prior migrations.

profiles_insert/"Users can insert their own profile." (from the two prior migrations) already enforce auth.uid() = id on INSERT, so users_create_own_profile is functionally redundant. More importantly, unlike 20260518000002_role_management.sql and its DROP POLICY IF EXISTS pattern, none of the policies in this file are preceded by a drop, so re-applying this migration (e.g., in a fresh environment reset or repair) will fail with "policy already exists."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260729000000_add_profiles_rls_policy.sql` around lines
34 - 38, Remove the redundant users_create_own_profile INSERT policy, since
profiles_insert and "Users can insert their own profile." already enforce the
same condition. Add DROP POLICY IF EXISTS guards before each policy created in
this migration, following the established role-management migration pattern so
reapplication succeeds.

21-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Service-role policies are dead code — service_role always bypasses RLS in Supabase.

Supabase docs state Also note that adding service_role in RLS policies does nothing. Since the service role never evaluates policies at all, service_role_read_all_profiles and service_role_update_profiles have no effect and just add misleading documentation-as-code about the security posture (line 44 comment implies these policies are what grant service-role access).

Consider removing these two policies and replacing the comment with a note that service_role access is inherent to the role, not policy-driven.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260729000000_add_profiles_rls_policy.sql` around lines
21 - 32, Remove the service_role_read_all_profiles and
service_role_update_profiles policies from the migration, since service_role
access bypasses RLS inherently. Replace their explanatory comments with a
concise note stating that service_role access is provided by the role and is not
policy-driven.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@supabase/migrations/20260729000000_add_profiles_rls_policy.sql`:
- Around line 8-12: Update the migration containing users_read_own_profile to
drop the existing permissive SELECT policies profiles_select and "Public
profiles are viewable by everyone." with DROP POLICY IF EXISTS on profiles
before or alongside creating the restrictive policy. Preserve
users_read_own_profile as the policy allowing users to select only rows where
auth.uid() = id.
- Around line 14-19: The profiles UPDATE policy must not create a permissive
path around the earlier policy’s column-level WITH CHECK guards. Update the
migration to either drop and replace the existing “Users can update their own
profile.” policy with equivalent ownership and column protections, or omit
users_update_own_profile when the existing policy already permits updates to
owned profiles.

---

Nitpick comments:
In `@supabase/migrations/20260729000000_add_profiles_rls_policy.sql`:
- Around line 34-38: Remove the redundant users_create_own_profile INSERT
policy, since profiles_insert and "Users can insert their own profile." already
enforce the same condition. Add DROP POLICY IF EXISTS guards before each policy
created in this migration, following the established role-management migration
pattern so reapplication succeeds.
- Around line 21-32: Remove the service_role_read_all_profiles and
service_role_update_profiles policies from the migration, since service_role
access bypasses RLS inherently. Replace their explanatory comments with a
concise note stating that service_role access is provided by the role and is not
policy-driven.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 10ecef92-b6b8-44de-ba3b-0b31e5591447

📥 Commits

Reviewing files that changed from the base of the PR and between 3565841 and f5ae513.

📒 Files selected for processing (1)
  • supabase/migrations/20260729000000_add_profiles_rls_policy.sql

Comment on lines +8 to +12
-- Policy: Users can read their own profile
CREATE POLICY "users_read_own_profile"
ON profiles
FOR SELECT
USING (auth.uid() = id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

This does not fix the exposed-PII vulnerability: old permissive USING (true) SELECT policies are never dropped.

public.profiles already has two permissive SELECT policies from prior migrations — profiles_select (supabase/migrations/20260518000002_role_management.sql) and "Public profiles are viewable by everyone." (supabase/migrations/20260617000000_consolidate_rls_policies.sql) — both USING (true). PostgreSQL combines multiple permissive policies for the same command with OR, so adding users_read_own_profile alongside them changes nothing: any role with SELECT grant (including anon) can still read every row via the pre-existing true policy. This migration must DROP POLICY IF EXISTS on the old permissive SELECT policies before/while adding the new restrictive one, or the anon-key PII exposure from issue #1870 remains unpatched.

🛡️ Proposed fix
+DROP POLICY IF EXISTS "profiles_select" ON public.profiles;
+DROP POLICY IF EXISTS "Public profiles are viewable by everyone." ON public.profiles;
+
 -- Policy: Users can read their own profile
+DROP POLICY IF EXISTS "users_read_own_profile" ON public.profiles;
 CREATE POLICY "users_read_own_profile"
   ON profiles
   FOR SELECT
   USING (auth.uid() = id);
#!/bin/bash
# Confirm the permissive SELECT/UPDATE/INSERT policies from prior migrations still exist and aren't dropped anywhere.
fd -e sql . supabase/migrations | sort
rg -n -A2 'CREATE POLICY' supabase/migrations/20260518000002_role_management.sql supabase/migrations/20260617000000_consolidate_rls_policies.sql
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260729000000_add_profiles_rls_policy.sql` around lines
8 - 12, Update the migration containing users_read_own_profile to drop the
existing permissive SELECT policies profiles_select and "Public profiles are
viewable by everyone." with DROP POLICY IF EXISTS on profiles before or
alongside creating the restrictive policy. Preserve users_read_own_profile as
the policy allowing users to select only rows where auth.uid() = id.

Comment on lines +14 to +19
-- Policy: Users can update their own profile
CREATE POLICY "users_update_own_profile"
ON profiles
FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== migration files =="
git ls-files | grep -E 'supabase/migrations/.*(add_profiles_rls_policy|consolidate_rls_policies)\.sql$' || true

echo
echo "== target migration =="
cat -n supabase/migrations/20260729000000_add_profiles_rls_policy.sql

echo
echo "== consolidated RLS migration candidates =="
for f in supabase/migrations/20260617000000_consolidate_rls_policies.sql; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f"
  fi
done

echo
echo "== all profile UPDATE policies =="
rg -n 'CREATE POLICY .*on profiles|FOR UPDATE|WITH CHECK|profiles' supabase/migrations || true

Repository: durdana3105/peer-learning

Length of output: 1936


🌐 Web query:

PostgreSQL RLS policy multiple WITH CHECK clauses combined with OR UPDATE

💡 Result:

In PostgreSQL, an UPDATE operation relies on both USING and WITH CHECK expressions to manage row access and modification [1][2]. When multiple policies are defined for an UPDATE operation, PostgreSQL combines their expressions according to specific rules based on the type of policy (PERMISSIVE vs. RESTRICTIVE) [1][3]. 1. Understanding USING and WITH CHECK for UPDATE The USING expression determines which existing rows the UPDATE operation can see and modify [1][2]. The WITH CHECK expression defines the constraints that the new (updated) row values must satisfy to be successfully stored back into the table [1][2]. If no explicit WITH CHECK clause is provided for an UPDATE policy, the USING clause is used for both purposes [2][4]. 2. Combining Multiple Policies When multiple policies apply to the same command (such as UPDATE), they are combined as follows: - Permissive vs. Restrictive: PostgreSQL distinguishes between PERMISSIVE policies (the default) and RESTRICTIVE policies [1][3]. - PERMISSIVE Policies: All applicable PERMISSIVE policy expressions for a specific command type are combined using the OR operator [1][3]. - RESTRICTIVE Policies: All applicable RESTRICTIVE policy expressions are combined using the AND operator [1][3]. - Final Combination: The result of the combined PERMISSIVE policies and the combined RESTRICTIVE policies are then joined using the AND operator [1][3]. 3. Combining Different Command Types If an operation requires multiple permissions (e.g., an UPDATE command typically requires both SELECT and UPDATE permissions), PostgreSQL combines the expressions for each command type first using the rules above, and then joins those results using the AND operator [1][3]. In practice, if you define multiple UPDATE policies, the database ensures that at least one PERMISSIVE policy grants access (or evaluates to true) and that all RESTRICTIVE policies are satisfied [1][3]. If you use multiple WITH CHECK clauses across different policies, they effectively become part of the collective set of constraints that must be satisfied for the update to proceed [1][3].

Citations:


🌐 Web query:

PostgreSQL CREATE POLICY "Users can update their own profile" WITH CHECK profiles auth.uid

💡 Result:

To correctly implement a Row Level Security (RLS) policy that allows users to update only their own profile, it is recommended to use both USING and WITH CHECK clauses [1][2][3]. The USING clause determines which existing rows the user has permission to update, while the WITH CHECK clause ensures that the resulting row after the update still satisfies the policy constraints [1][3][4]. Omitting the USING clause in an UPDATE policy can create a security vulnerability where unauthorized users may be able to modify rows they should not have access to [2]. Below is the recommended SQL syntax for a profiles table: CREATE POLICY "Users can update their own profile" ON profiles FOR UPDATE TO authenticated USING (auth.uid = id) WITH CHECK (auth.uid = id); In this example: - auth.uid() is a function commonly used in Supabase to retrieve the current user's ID from their JWT [5][6][4]. - id should be replaced with the column name used to store the user's ID (e.g., user_id) [4]. - The TO authenticated clause restricts this policy to logged-in users [1][5]. If no WITH CHECK expression is explicitly defined, PostgreSQL defaults to using the USING expression for both checks [1][7]. However, explicitly defining both is considered a best practice to prevent data hijacking and ensure clarity regarding which condition governs row visibility versus row modification [2][3]. Note that for an UPDATE operation to function as expected, a corresponding SELECT policy must typically also be in place [1][8].

Citations:


Avoid weakening profile UPDATE RLS with a permissive duplicate policy.

If the earlier "Users can update their own profile." UPDATE policy contains column-level WITH CHECK guards, creating another unrestricted permissive UPDATE policy on profiles without dropping or replacing it will let users bypass those guards, because permissive update policies combine with OR. Either drop/replace the prior policy with an equivalent protected version, or omit this new one if the existing policy already allows owned profile updates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260729000000_add_profiles_rls_policy.sql` around lines
14 - 19, The profiles UPDATE policy must not create a permissive path around the
earlier policy’s column-level WITH CHECK guards. Update the migration to either
drop and replace the existing “Users can update their own profile.” policy with
equivalent ownership and column protections, or omit users_update_own_profile
when the existing policy already permits updates to owned profiles.

@anshul23102
anshul23102 force-pushed the fix/issue-1870-profiles-rls branch 2 times, most recently from 2ad4cfe to 5d6213a Compare July 29, 2026 12:43
@durdana3105
durdana3105 merged commit ff31b7f into durdana3105:main Jul 31, 2026
1 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Profiles table missing RLS policy, all user emails readable via anon Supabase key

2 participants