Skip to content

security(rls): restrict courses/professors UPDATE to admin#75

Open
roshanraj9136 wants to merge 1 commit into
OpenLake:mainfrom
roshanraj9136:fix/rls-restrict-course-professor-update
Open

security(rls): restrict courses/professors UPDATE to admin#75
roshanraj9136 wants to merge 1 commit into
OpenLake:mainfrom
roshanraj9136:fix/rls-restrict-course-professor-update

Conversation

@roshanraj9136

@roshanraj9136 roshanraj9136 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Vulnerability

The RLS UPDATE policies on courses and professors tables are:

CREATE POLICY course_update ON courses
  FOR UPDATE USING (true) WITH CHECK (true);

CREATE POLICY professor_update ON professors
  FOR UPDATE USING (true) WITH CHECK (true);

This means any authenticated user can update any course or professor record — change names, ratings, departments, emails, etc. The original comment says 'allow trigger updates', but triggers defined with SECURITY DEFINER bypass RLS entirely, so this permissive policy only benefits attackers.

Impact

A malicious user with a valid session could:

  • Modify course/professor ratings directly in the DB
  • Change professor email addresses or names
  • Corrupt department or credit data

Fix

Restrict UPDATE to is_admin(), matching the existing INSERT and DELETE policies:

CREATE POLICY course_update ON courses
  FOR UPDATE USING (is_admin()) WITH CHECK (is_admin());

CREATE POLICY professor_update ON professors
  FOR UPDATE USING (is_admin()) WITH CHECK (is_admin());

Important

This fixes the migration file. If the database is already deployed, you also need to run this against the live DB:

DROP POLICY course_update ON courses;
CREATE POLICY course_update ON courses FOR UPDATE USING (is_admin()) WITH CHECK (is_admin());

DROP POLICY professor_update ON professors;
CREATE POLICY professor_update ON professors FOR UPDATE USING (is_admin()) WITH CHECK (is_admin());

File changed (1)

src/migrations/migration.sql — 2 lines changed

Summary by CodeRabbit

Release Notes

  • Chores
    • Restricted update permissions for courses and professors. Only administrators can now modify these entities to prevent unauthorized changes to critical data.

The UPDATE policies on courses and professors used USING (true) WITH
CHECK (true), allowing any authenticated user to modify any course or
professor record (name, ratings, department, etc.).

The comment said 'allow trigger updates' but triggers defined with
SECURITY DEFINER bypass RLS entirely, so the permissive policy only
benefited attackers. Restrict to is_admin() to match INSERT and DELETE
policies.
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

@roshanraj9136 is attempting to deploy a commit to the OpenLake_Website Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This migration tightens Row-Level Security for courses and professors tables by restricting UPDATE operations to admin users only. Both the course_update and professor_update policies are changed from public access (true conditions) to admin-only access via is_admin() checks.

Changes

RLS Security Tightening

Layer / File(s) Summary
RLS update policy restrictions
src/migrations/migration.sql
course_update and professor_update policies restrict UPDATE access to admins by replacing permissive USING (true) WITH CHECK (true) conditions with USING (is_admin()) WITH CHECK (is_admin()).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~5 minutes

Poem

🐰 The rabbit hops through SQL lands,
Where security now takes firm stands,
No more public updates run free,
Admins only—that's the key! ✨

🚥 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 accurately and concisely summarizes the main security fix: restricting UPDATE permissions on courses and professors tables to admin users only.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 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 and usage tips.

@Ashish-Kumar-Dash

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@src/migrations/migration.sql`:
- Around line 395-396: The migration bootstrap defines RLS policies
course_update and professor_update but there is no forward migration to update
those policies on existing DBs; add a new forward SQL migration that drops each
policy if it exists and recreates it with the secure definition (DROP POLICY IF
EXISTS course_update ON courses; CREATE POLICY course_update ON courses FOR
UPDATE USING (is_admin()) WITH CHECK (is_admin()); and similarly for
professor_update on the professors table) so deployed databases get the patched
policies applied; include both DROP and CREATE statements in the new migration
file and ensure it is ordered after existing schema migrations.
- Around line 395-396: The RLS breakage arises because the trigger functions
update_course_ratings and update_professor_ratings perform UPDATEs on
courses/professors while those UPDATE policies (course_update and
professor_update) require is_admin(), but the functions are not SECURITY DEFINER
so non-admin review writes get blocked; fix by modifying the trigger function
definitions in the migration to declare them as SECURITY DEFINER (add the
SECURITY DEFINER clause to update_course_ratings and update_professor_ratings),
ensure those functions are owned by an administrative role that satisfies
is_admin() (or adjust their owner accordingly after creation), and set a safe
search_path inside the function to avoid privilege escalation. This change keeps
the existing course_update and professor_update policies intact while allowing
the trigger-side UPDATEs to run with admin privileges.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 39b4e4ef-3c26-4dab-9811-ae248464423b

📥 Commits

Reviewing files that changed from the base of the PR and between cd02e34 and efd5e62.

📒 Files selected for processing (1)
  • src/migrations/migration.sql

Comment on lines 395 to +396
CREATE POLICY course_update ON courses
FOR UPDATE USING (true) WITH CHECK (true);
FOR UPDATE USING (is_admin()) WITH CHECK (is_admin());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f -e sql src/migrations
rg -n -C2 'DROP POLICY|ALTER POLICY|CREATE POLICY course_update|CREATE POLICY professor_update' src/migrations

Repository: OpenLake/RateMyCourse

Length of output: 1058


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) List SQL files under src/migrations
echo "== SQL files =="
fd -t f -e sql src/migrations || true

# 2) Show all occurrences of the policy names across the repo
echo
echo "== Occurrences of course_update =="
rg -n --hidden --no-ignore -S "course_update" . || true

echo
echo "== Occurrences of professor_update =="
rg -n --hidden --no-ignore -S "professor_update" . || true

# 3) Search for policy update statements around bootstrap/drop/recreate schema markers
echo
echo "== Policy-related statements in migrations files =="
fd -t f -e sql src/migrations | xargs -r rg -n --hidden --no-ignore -S "DROP POLICY|ALTER POLICY|CREATE POLICY" || true

# 4) Identify migration framework hints (optional, lightweight)
echo
echo "== Migration framework hints (best-effort) =="
rg -n --hidden --no-ignore -S "flyway|goose|liquibase|knex|sequelize|prisma|migrate|migration" src/migrations . || true

Repository: OpenLake/RateMyCourse

Length of output: 1500


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SQL files anywhere with migrations in path =="
find . -type f -name '*.sql' -path '*migrations*' | sed 's|^\./||' | sort

echo
echo "== All DROP/ALTER/CREATE POLICY statements across repo =="
rg -n --hidden --no-ignore -S "DROP POLICY|ALTER POLICY|CREATE POLICY" . | head -n 200

echo
echo "== CREATE POLICY statements that use is_admin() =="
rg -n --hidden --no-ignore -S "CREATE POLICY" src/migrations/migration.sql
rg -n --hidden --no-ignore -S "is_admin\\(\\)" src/migrations/migration.sql

echo
echo "== migration.sql header (first 60 lines) =="
nl -ba src/migrations/migration.sql | sed -n '1,60p'

echo
echo "== migration.sql policy area (lines 360-430) =="
nl -ba src/migrations/migration.sql | sed -n '360,430p'

Repository: OpenLake/RateMyCourse

Length of output: 4399


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file stats =="
wc -l src/migrations/migration.sql
echo

echo "== first 120 lines =="
cat -n src/migrations/migration.sql | sed -n '1,120p'
echo

echo "== lines 330-460 (policy area) =="
cat -n src/migrations/migration.sql | sed -n '330,460p'
echo

echo "== does migration.sql contain any 'DROP TABLE'/'CREATE TABLE' schema drop? =="
rg -n --no-ignore --hidden -S "DROP TABLE|CREATE TABLE|DROP SCHEMA|CREATE SCHEMA" src/migrations/migration.sql || true

echo
echo "== does it drop/recreate policies earlier? =="
rg -n --no-ignore --hidden -S "DROP POLICY|CREATE POLICY" src/migrations/migration.sql | head -n 60

Repository: OpenLake/RateMyCourse

Length of output: 11715


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Directories named migrations (max depth 4) =="
find . -maxdepth 4 -type d -iname '*migrations*' | sed 's|^\./||' | sort

echo
echo "== SQL files with 'migration' in filename (full repo) =="
find . -type f -iname '*migration*.sql' | sed 's|^\./||' | sort

echo
echo "== SQL files in root-level migrations dirs (supabase, db, etc.) =="
find . -type f -name '*.sql' -path '*migrations*' | sed 's|^\./||' | sort

Repository: OpenLake/RateMyCourse

Length of output: 313


Add a forward migration to patch existing DB RLS policies (course_update/professor_update)

src/migrations/migration.sql is a bootstrap script that drops/recreates tables; it also defines course_update (395-396) and professor_update (408-409), but there is no separate forward migration to update policies on already-deployed databases. Without an in-repo forward migration, stale vulnerable policies may remain active.

-- New forward migration (example)
DROP POLICY IF EXISTS course_update ON courses;
CREATE POLICY course_update ON courses
  FOR UPDATE USING (is_admin()) WITH CHECK (is_admin());

DROP POLICY IF EXISTS professor_update ON professors;
CREATE POLICY professor_update ON professors
  FOR UPDATE USING (is_admin()) WITH CHECK (is_admin());
🤖 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 `@src/migrations/migration.sql` around lines 395 - 396, The migration bootstrap
defines RLS policies course_update and professor_update but there is no forward
migration to update those policies on existing DBs; add a new forward SQL
migration that drops each policy if it exists and recreates it with the secure
definition (DROP POLICY IF EXISTS course_update ON courses; CREATE POLICY
course_update ON courses FOR UPDATE USING (is_admin()) WITH CHECK (is_admin());
and similarly for professor_update on the professors table) so deployed
databases get the patched policies applied; include both DROP and CREATE
statements in the new migration file and ensure it is ordered after existing
schema migrations.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
from pathlib import Path
import re

sql = Path("src/migrations/migration.sql").read_text()

for fn in ("update_course_ratings", "update_professor_ratings"):
    m = re.search(
        rf"CREATE OR REPLACE FUNCTION\s+{fn}\s*\(\)\s*RETURNS\s+TRIGGER(?P<header>.*?)AS\s+\$\$",
        sql,
        flags=re.IGNORECASE | re.DOTALL,
    )
    if not m:
        print(f"{fn}: NOT FOUND")
        continue
    header = " ".join(m.group("header").split()).upper()
    print(f"{fn}: SECURITY DEFINER present? {'SECURITY DEFINER' in header}")
PY

Repository: OpenLake/RateMyCourse

Length of output: 178


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/migrations/migration.sql"

# Show the policy area around 395-420
echo "=== RLS policy area (lines 380-430) ==="
nl -ba "$FILE" | sed -n '380,430p'

# Show trigger function definitions area where review comment claims they are defined (lines 120-260)
echo
echo "=== Trigger function definitions area (lines 100-280) ==="
nl -ba "$FILE" | sed -n '100,280p'

# Extract direct evidence of triggers on reviews and updates to courses/professors
echo
echo "=== Find triggers on reviews and updates to courses/professors ==="
rg -n "CREATE TRIGGER|ON reviews" "$FILE"
echo
rg -n "UPDATE\s+courses|UPDATE\s+professors" "$FILE" | head -n 50

Repository: OpenLake/RateMyCourse

Length of output: 166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/migrations/migration.sql"

echo "=== RLS policy area (lines 360-430) ==="
awk 'NR>=360 && NR<=430 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "=== Trigger function definitions area (lines 80-280) ==="
awk 'NR>=80 && NR<=280 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "=== Locations: CREATE TRIGGER and ON reviews ==="
rg -n "CREATE TRIGGER|ON reviews" "$FILE" || true

echo
echo "=== Locations: UPDATE courses / UPDATE professors ==="
rg -n "UPDATE\s+courses|UPDATE\s+professors" "$FILE" || true

echo
echo "=== Function headers (repeat) ==="
python3 - <<'PY'
from pathlib import Path
import re
sql = Path("src/migrations/migration.sql").read_text()
for fn in ("update_course_ratings", "update_professor_ratings"):
    m = re.search(rf"CREATE OR REPLACE FUNCTION\s+{fn}\s*\(\)\s*RETURNS\s+TRIGGER(?P<header>.*?)AS\s+\$\$",
                  sql, flags=re.IGNORECASE|re.DOTALL)
    if not m:
        print(f"{fn}: NOT FOUND")
        continue
    header = " ".join(m.group("header").split()).upper()
    print(f"{fn}: SECURITY DEFINER present? {'SECURITY DEFINER' in header}")
    # Print a compact header excerpt
    print(f"{fn}: header excerpt: {header[:220]}...")
PY

Repository: OpenLake/RateMyCourse

Length of output: 11146


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/migrations/migration.sql"

echo "=== Review policies area (lines 420-470) ==="
awk 'NR>=420 && NR<=470 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "=== Trigger statements for rating/votes/flag/sanitize (lines 290-360) ==="
awk 'NR>=290 && NR<=360 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "=== Target table column names check (spot relevant updates) ==="
rg -n "overall_rating|difficulty_rating|workload_rating|review_count|knowledge_rating|teaching_rating|approachability_rating" "$FILE" | head -n 50

Repository: OpenLake/RateMyCourse

Length of output: 5979


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/migrations/migration.sql"

echo "=== Search for FORCE ROW LEVEL SECURITY / BYPASSRLS / OWNER TO ==="
rg -n "FORCE ROW LEVEL SECURITY|BYPASSRLS|OWNER TO" "$FILE" || true

echo
echo "=== Search for ALTER TABLE ... ENABLE ROW LEVEL SECURITY ==="
rg -n "ALTER TABLE .* ENABLE ROW LEVEL SECURITY" "$FILE" || true

echo
echo "=== Quick context: course/professor policies (just in case) ==="
rg -n "CREATE POLICY (course_update|professor_update) " "$FILE" -n -C 2

Repository: OpenLake/RateMyCourse

Length of output: 947


RLS breakage: reviews triggers update courses/professors, but those UPDATE policies are admin-only while trigger functions aren’t SECURITY DEFINER

  • course_update / professor_update restrict UPDATE courses / UPDATE professors to is_admin() (lines 395-396, 408-409).
  • update_course_ratings / update_professor_ratings run UPDATE courses / UPDATE professors from AFTER INSERT OR UPDATE OR DELETE ON reviews triggers, but both functions are defined without SECURITY DEFINER, so non-admin review writes can be rolled back by RLS when the trigger-side UPDATE is evaluated.
🔧 Proposed fix
-CREATE OR REPLACE FUNCTION update_course_ratings() 
-RETURNS TRIGGER AS $$
+CREATE OR REPLACE FUNCTION update_course_ratings() 
+RETURNS TRIGGER
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = public, pg_temp
+AS $$
@@
-END;
-$$ LANGUAGE plpgsql;
+END;
+$$;

-CREATE OR REPLACE FUNCTION update_professor_ratings() 
-RETURNS TRIGGER AS $$
+CREATE OR REPLACE FUNCTION update_professor_ratings() 
+RETURNS TRIGGER
+LANGUAGE plpgsql
+SECURITY DEFINER
+SET search_path = public, pg_temp
+AS $$
@@
-END;
-$$ LANGUAGE plpgsql;
+END;
+$$;
🤖 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 `@src/migrations/migration.sql` around lines 395 - 396, The RLS breakage arises
because the trigger functions update_course_ratings and update_professor_ratings
perform UPDATEs on courses/professors while those UPDATE policies (course_update
and professor_update) require is_admin(), but the functions are not SECURITY
DEFINER so non-admin review writes get blocked; fix by modifying the trigger
function definitions in the migration to declare them as SECURITY DEFINER (add
the SECURITY DEFINER clause to update_course_ratings and
update_professor_ratings), ensure those functions are owned by an administrative
role that satisfies is_admin() (or adjust their owner accordingly after
creation), and set a safe search_path inside the function to avoid privilege
escalation. This change keeps the existing course_update and professor_update
policies intact while allowing the trigger-side UPDATEs to run with admin
privileges.

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