Skip to content

fix: resolve IDOR vulnerability exposing private user data (#1853) - #1866

Open
atul-upadhyay-7 wants to merge 3 commits into
durdana3105:mainfrom
atul-upadhyay-7:fix/idor-vulnerability-1853
Open

fix: resolve IDOR vulnerability exposing private user data (#1853)#1866
atul-upadhyay-7 wants to merge 3 commits into
durdana3105:mainfrom
atul-upadhyay-7:fix/idor-vulnerability-1853

Conversation

@atul-upadhyay-7

@atul-upadhyay-7 atul-upadhyay-7 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes the Insecure Direct Object Reference (IDOR) vulnerability described in #1853. The fix ensures that the server validates user ownership before returning or allowing modification of private user data.

Problem

The API endpoints for fetching user match data and private profile details relied on user IDs provided in request payloads or URL parameters without adequate server-side authorization checks. Any authenticated user could potentially modify the ID parameter to access private data belonging to other users.

Changes

Backend

  • New requireOwnershipOrAdmin middleware (backend/middlewares/requireAuth.js): Reusable middleware that validates resource ownership by comparing the authenticated user's JWT ID against the target resource's owner ID. Includes strict UUID v4 format validation and admin bypass.
  • New secure profile endpoints (backend/routes/users.js):
    • GET /api/users/:userId/profile — Returns public profile fields to all authenticated users, but private fields (email, last_active, etc.) only to the profile owner or admins.
    • PUT /api/users/:userId/profile — Profile update with ownership enforcement via requireOwnershipOrAdmin middleware.
  • Hardened notification endpoint (backend/controllers/notificationController.js): Added strict UUID v4 validation on user_id parameter, explicit IDOR blocking with audit logging.

Database

  • New migration (supabase/migrations/20260727000000_idor_fix_private_profile_access.sql): Documents the profiles table RLS security boundary.

Frontend

  • Profile.tsx and EditProfile.tsx: Updated to use the new server-side /api/users/:id/profile endpoints instead of direct Supabase client calls.

Testing

  • TypeScript compilation: ✅ Passes cleanly
  • Security tests: ✅ 37/37 pass
  • Linting: ✅ 0 errors
  • Backend tests: ✅ No regressions from this change

Note: The 5 failing CI checks (sonner.tsx, theme-provider.tsx, MarkdownRenderer.tsx, Testimonials.tsx, docs.test.js) are pre-existing issues unrelated to this change — none of these files were modified by this PR.

Fixes #1853

Summary by CodeRabbit

  • New Features

    • Added secure profile viewing and editing through authenticated application endpoints.
    • Profile updates support name, bio, and skills with input validation.
    • Profile visibility now distinguishes between public information and private details.
    • Profile pages and editing screens now provide clearer error handling and success feedback.
  • Bug Fixes

    • Strengthened authorization to prevent users from viewing or modifying other users’ profiles.
    • Improved authentication checks and notification recipient validation.
    • Added stricter validation for profile and user identifiers.

- Enforce strict algorithm validation (reject 'none' algorithm, only accept HS256)
- Add timing-safe signature comparison to prevent timing attacks
- Validate JWT claims: exp, iat, iss, aud
- Add SUPABASE_JWT_SECRET to env config and .env.example
- Apply timing-safe comparison to cron and webhook secret verification
- Defense-in-depth against crafted JWTs with elevated roles
…05#1853)

- Add requireOwnershipOrAdmin middleware for server-side resource ownership validation
- Add UUID format validation on all user ID parameters to prevent injection
- Add secure /api/users/:userId/profile GET endpoint with field-level access control
  (public fields for other users, private fields for profile owner/admins only)
- Add secure /api/users/:userId/profile PUT endpoint with ownership enforcement
- Strengthen notification endpoint authorization with strict UUID validation
  and explicit IDOR blocking with audit logging
- Create SQL migration for profiles RLS hardening with documented security boundary
- Update Profile.tsx and EditProfile.tsx to use server-side endpoints instead of
  direct Supabase client calls, ensuring all profile access is authorized server-side

Fixes durdana3105#1853
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

@atul-upadhyay-7 is attempting to deploy a commit to the durdana3105's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR hardens local JWT verification, adds ownership-or-admin authorization, introduces protected profile endpoints, migrates profile pages to those endpoints, validates notification targets, and updates profile SELECT policies.

Changes

Security and profile access hardening

Layer / File(s) Summary
JWT verification and auth context
backend/config.js, backend/middlewares/requireAuth.js
JWT algorithm, signature, and claim validation are tightened, and verified roles are added to req.user.
Ownership middleware and profile endpoints
backend/middlewares/requireAuth.js, backend/routes/users.js
New ownership/admin checks and authenticated profile GET/PUT endpoints validate UUIDs, filter fields, and restrict updates.
Client profile API migration
src/pages/EditProfile.tsx, src/pages/Profile.tsx
Profile pages replace direct table access with authenticated server API requests and response validation.
Notification target validation
backend/controllers/notificationController.js
Notification targets require UUID v4 format, and non-admin callers can target only themselves.
Profile SELECT policy migration
supabase/migrations/20260727000000_idor_fix_private_profile_access.sql
Existing profile SELECT policies are replaced with explicit authenticated and anonymous policies and backend-access comments.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProfilePage
  participant SupabaseAuth
  participant UsersAPI
  participant RequireAuth
  participant OwnershipCheck
  participant SupabaseAdmin
  ProfilePage->>SupabaseAuth: Get access token
  ProfilePage->>UsersAPI: GET or PUT /api/users/:userId/profile
  UsersAPI->>RequireAuth: Verify Bearer JWT
  RequireAuth->>OwnershipCheck: Check owner or admin for PUT
  OwnershipCheck->>SupabaseAdmin: Read or update profile
  SupabaseAdmin-->>UsersAPI: Profile response
  UsersAPI-->>ProfilePage: JSON result
Loading

Possibly related PRs

Suggested labels: gssoc26, gssoc:approved, quality:clean, type:bug, level:beginner

Suggested reviewers: arshvermagit, riddhima25bet10005-a11y

🚥 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 is concise and accurately summarizes the main fix: resolving an IDOR vulnerability exposing private user data.
Linked Issues check ✅ Passed The PR adds server-side ownership checks and session-based authorization for the vulnerable profile access paths, matching #1853's requirements.
Out of Scope Changes check ✅ Passed No clear unrelated code changes were introduced beyond the IDOR remediation scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@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: 7

🧹 Nitpick comments (6)
backend/controllers/notificationController.js (1)

64-76: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Make webhook privilege explicit rather than inferring it from a missing user.

req.user === undefined is treated as permission to target any user. The current route establishes webhook authentication, but a future route mount without that middleware would silently bypass ownership checks. Set an explicit trusted-auth marker in verifyNotificationAuth, and reject requests that have neither that marker nor an authenticated user.

🤖 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 `@backend/controllers/notificationController.js` around lines 64 - 76, Update
verifyNotificationAuth to set an explicit trusted webhook-authentication marker
on the request after successful webhook validation. In the notification
authorization block, allow cross-user targeting only for admins or requests
carrying that marker; reject requests with neither an authenticated req.user nor
the trusted marker, while preserving self-targeting for standard authenticated
users.
backend/config.js (2)

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

SITE_URL lost its URL-format validation.

Per the change summary this went from z.string().url() to a plain optional string, while sibling URL vars (FRONTEND_URL, CLIENT_URL, PASSWORD_RESET_BASE_URL) keep .url(). If the relaxation was to allow a non-absolute value, it's worth a comment; otherwise restore .url() for consistency.

Proposed change
-  SITE_URL: z.string().optional(),
+  SITE_URL: z.string().url().optional(),
🤖 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 `@backend/config.js` at line 17, Update the SITE_URL schema definition to
restore URL-format validation with the same optional URL pattern used by
FRONTEND_URL, CLIENT_URL, and PASSWORD_RESET_BASE_URL. Only retain a plain
optional string if the non-absolute-value requirement is intentional and
document that exception.

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

Misleading validation message on an optional field.

.min(1, "SUPABASE_JWT_SECRET is required for JWT verification").optional() never surfaces that message when the variable is absent — only when it is present-but-empty. If local JWT verification is meant to be mandatory in production, enforce it with a superRefine on NODE_ENV instead of relying on the message text.

🤖 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 `@backend/config.js` at line 10, Update the SUPABASE_JWT_SECRET validation and
NODE_ENV configuration schema so JWT verification is required when running in
production, using superRefine to add the required-field issue conditionally.
Keep the secret optional outside production and replace the misleading
min-length message with wording appropriate for a present-but-empty value.
src/pages/EditProfile.tsx (1)

114-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The server's error message is parsed then thrown away.

Lines 114-117 carefully extract errorData.error, but the catch at Line 123 always shows the generic "Error updating profile". Surface the thrown message so actionable server responses (e.g. "Name must be 100 characters or fewer") reach the user.

🤖 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/pages/EditProfile.tsx` around lines 114 - 117, Update the error handling
in the profile submission flow around the response check and its catch block so
the caught Error message is displayed to the user instead of always using the
generic “Error updating profile” text. Preserve the server-derived message
created by the non-OK response branch, including actionable validation details.
src/pages/Profile.tsx (1)

94-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The profile fetch/save logic is now duplicated verbatim with src/pages/EditProfile.tsx.

Both pages independently do getSession() → build Authorization headers → fetch(${API_BASE_URL}/api/users/${id}/profile) → hand-roll res.ok error parsing. Extracting a small src/lib/profileApi.ts with getProfile(userId) / updateProfile(userId, payload) would keep the auth header and error-shape contract in one place as the backend endpoint evolves.

🤖 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/pages/Profile.tsx` around lines 94 - 127, The profile request flow in
handleSave is duplicated with EditProfile.tsx. Extract shared getProfile(userId)
and updateProfile(userId, payload) helpers into src/lib/profileApi.ts,
centralizing session retrieval, Authorization headers, profile endpoint fetches,
and res.ok error parsing; update both pages to use these helpers while
preserving their existing payload and UI behavior.
backend/routes/users.js (1)

152-170: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Allowlisted fields are copied through without type validation.

Only name/bio get a length check, and even that is skipped for non-strings (updates.name && typeof updates.name === "string"), so { name: { $x: 1 } } or a 10k-element skills array is forwarded straight to Postgres — producing a 500 at best, oversized rows at worst. Consider a small per-field schema (type + max length + max array size), e.g. with zod, which is already a dependency in this backend.

🤖 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 `@backend/routes/users.js` around lines 152 - 170, Add schema-based validation
for the allowlisted fields collected in updates, using the existing zod
dependency and defining each field’s expected type plus maximum string lengths
and array sizes. Validate updates before the database operation, reject
malformed values such as object-valued name or oversized skills with a 400
response, and remove reliance on truthiness-based checks that skip invalid
non-string values.
🤖 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 `@backend/middlewares/requireAuth.js`:
- Around line 302-312: Case-insensitive UUID validation is followed by
case-sensitive ownership checks, incorrectly rejecting uppercase UUIDs. In
backend/middlewares/requireAuth.js lines 302-312, normalize both req.user.id and
targetUserId before the ownership comparison; apply the same normalization to
both sides of the ownership check in backend/routes/users.js lines 106-112 so
owners are recognized consistently.
- Line 153: Update the requireAdminRole route middleware chain before
requireOwnershipOrAdmin so it performs the profile lookup and populates
req.roles from the application role source, including profiles.is_admin and
active-role status; do not rely on requireAuth’s req.user.role, which only
contains the Supabase RLS role. Ensure ownership and admin account holders
receive the same bypass behavior as other profile-authenticated requests.
- Around line 60-70: Update the JWT validation logic in the requireAuth
middleware: require a numeric payload.exp and reject tokens when it is missing
or expired, and replace the literal payload.iss === "supabase" expectation with
validation that accepts the hosted Supabase issuer URL format while still
rejecting invalid issuers. Preserve the existing iat validation.

In `@backend/routes/users.js`:
- Around line 172-184: Update the profile update flow after the existing
Supabase error check to detect when the returned profile from maybeSingle() is
null, and return a 404 response instead of success. Preserve the current 500
handling for Supabase errors and the successful response for non-null profiles
in the profile update handler.

In `@src/pages/EditProfile.tsx`:
- Line 29: Update the profile-loading effect around the no-session guard and its
catch path so both failures set the profile loading sentinel to an explicit
empty/error state before navigating to the login page. Add the required navigate
dependency to the effect dependency list, while preserving the existing
successful profile-loading flow.

In `@supabase/migrations/20260727000000_idor_fix_private_profile_access.sql`:
- Around line 21-24: Update the migration header comment to accurately describe
the SELECT policy and column-level protection, removing the claim that it only
documents the policy or otherwise overstates database-layer privacy. Preserve
the explanation that public row access remains intentional for
PublicPortfolio.tsx and peer discovery, while clarifying the actual scope of the
migration.
- Around line 30-45: Replace the blanket SELECT policies on public.profiles that
use USING (true) with column-restricted access for authenticated and anonymous
users. Revoke broad table SELECT permission and grant only the public profile
columns, or expose them through a public-only view, keeping the selected columns
synchronized with PUBLIC_PROFILE_FIELDS and excluding private fields such as
email, last_active, availability, and learning_goals.

---

Nitpick comments:
In `@backend/config.js`:
- Line 17: Update the SITE_URL schema definition to restore URL-format
validation with the same optional URL pattern used by FRONTEND_URL, CLIENT_URL,
and PASSWORD_RESET_BASE_URL. Only retain a plain optional string if the
non-absolute-value requirement is intentional and document that exception.
- Line 10: Update the SUPABASE_JWT_SECRET validation and NODE_ENV configuration
schema so JWT verification is required when running in production, using
superRefine to add the required-field issue conditionally. Keep the secret
optional outside production and replace the misleading min-length message with
wording appropriate for a present-but-empty value.

In `@backend/controllers/notificationController.js`:
- Around line 64-76: Update verifyNotificationAuth to set an explicit trusted
webhook-authentication marker on the request after successful webhook
validation. In the notification authorization block, allow cross-user targeting
only for admins or requests carrying that marker; reject requests with neither
an authenticated req.user nor the trusted marker, while preserving
self-targeting for standard authenticated users.

In `@backend/routes/users.js`:
- Around line 152-170: Add schema-based validation for the allowlisted fields
collected in updates, using the existing zod dependency and defining each
field’s expected type plus maximum string lengths and array sizes. Validate
updates before the database operation, reject malformed values such as
object-valued name or oversized skills with a 400 response, and remove reliance
on truthiness-based checks that skip invalid non-string values.

In `@src/pages/EditProfile.tsx`:
- Around line 114-117: Update the error handling in the profile submission flow
around the response check and its catch block so the caught Error message is
displayed to the user instead of always using the generic “Error updating
profile” text. Preserve the server-derived message created by the non-OK
response branch, including actionable validation details.

In `@src/pages/Profile.tsx`:
- Around line 94-127: The profile request flow in handleSave is duplicated with
EditProfile.tsx. Extract shared getProfile(userId) and updateProfile(userId,
payload) helpers into src/lib/profileApi.ts, centralizing session retrieval,
Authorization headers, profile endpoint fetches, and res.ok error parsing;
update both pages to use these helpers while preserving their existing payload
and UI behavior.
🪄 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: 9b718fcf-2443-4cd1-a356-5b983dbba5b6

📥 Commits

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

📒 Files selected for processing (7)
  • backend/config.js
  • backend/controllers/notificationController.js
  • backend/middlewares/requireAuth.js
  • backend/routes/users.js
  • src/pages/EditProfile.tsx
  • src/pages/Profile.tsx
  • supabase/migrations/20260727000000_idor_fix_private_profile_access.sql

Comment thread backend/middlewares/requireAuth.js
Comment thread backend/middlewares/requireAuth.js
Comment thread backend/middlewares/requireAuth.js
Comment thread backend/routes/users.js
Comment thread src/pages/EditProfile.tsx
@durdana3105

Copy link
Copy Markdown
Owner

please resolve merge conflicts

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.

Major Vulnerability: Insecure Direct Object Reference (IDOR) Exposing Private User Data

2 participants