Skip to content

fix(#98): verify ownership before deleting a resource#104

Merged
durdana3105 merged 1 commit into
durdana3105:mainfrom
anshul23102:fix/98-deleteResource-ownership
May 26, 2026
Merged

fix(#98): verify ownership before deleting a resource#104
durdana3105 merged 1 commit into
durdana3105:mainfrom
anshul23102:fix/98-deleteResource-ownership

Conversation

@anshul23102
Copy link
Copy Markdown
Contributor

Closes #98

Problem

deleteResource accepted a resourceId and fileUrl from the caller and acted on both without verifying the caller owns the resource. An attacker knowing a victim's resource DB row ID could call deleteResource(victimId, attackerFilePath):

  1. Storage delete succeeds (attacker's own path, allowed by RLS)
  2. DB delete fails (victim's row, blocked by RLS)
  3. Victim's DB row now points to a permanently deleted storage file - silently broken with no error surfaced

Changes

src/lib/deleteResource.ts

  • Resolve the authenticated user via supabase.auth.getUser() before any mutation
  • Fetch the resource using both .eq("id", resourceId) and .eq("uploaded_by", user.id) - returns an error if the row is not found or not owned by the caller
  • Use the file_url from the fetched DB row rather than the caller-supplied argument, eliminating the cross-user storage/DB mismatch vector
  • Remove fileUrl from the function signature (it is now derived from the DB)

src/components/resources/ResourceCard.tsx

  • Update the deleteResource call to pass only resource.id (no file_url argument)

Before / After

// Before - no ownership check, trusts caller-supplied fileUrl
const deleteResource = async (resourceId: string, fileUrl: string) => {
  await supabase.storage.from("resources").remove([fileUrl]);
  await supabase.from("resources").delete().eq("id", resourceId);
};

// After - ownership verified, fileUrl sourced from DB
const deleteResource = async (resourceId: string) => {
  const { data: { user } } = await supabase.auth.getUser();
  const { data: resource } = await supabase
    .from("resources").select("id, file_url")
    .eq("id", resourceId).eq("uploaded_by", user.id).single();
  if (!resource) return { success: false, error: "Not found or unauthorized" };
  // proceed with resource.file_url
};

deleteResource previously accepted a resourceId and fileUrl from the
caller and acted on both without any authorization check. An attacker
could pass a victim's resourceId alongside their own fileUrl, causing
the storage delete to succeed (their file) while the DB delete failed
(victim's row protected by RLS), leaving the victim's row pointing at
a permanently missing file.

Fix:
- Look up the resource by id AND uploaded_by = auth.uid() before
  touching anything. Return a 'not found or unauthorized' error if the
  row is not owned by the caller.
- Derive file_url from the DB row instead of trusting caller input,
  eliminating the cross-user storage/DB mismatch vector.
- Remove the fileUrl parameter from the function signature; update the
  only caller (ResourceCard.tsx) accordingly.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 25, 2026

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

@durdana3105 durdana3105 merged commit e8382c4 into durdana3105:main May 26, 2026
1 check failed
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.

[BUG] deleteResource performs no ownership check — partial-delete IDOR lets an authenticated user orphan another user's resource file

2 participants