It started the way most things do — curiosity and Burp Suite running in the background.
A friend of mine from uni had built something genuinely useful: a platform where students could access past exams, course materials, and build a leaderboard of academic contributions. He'd put real effort into it. He also trusted me enough to ask me to poke at it. That was his first mistake. (Kidding :))
I want to be clear upfront: this was a fully authorized engagement. My friend knew exactly what I was doing and gave explicit permission before I touched anything. What I found surprised even me — not because the bugs were unique, but because of how quickly they stacked up.
This is a writeup of four findings, ranging from silly to genuinely concerning.
The Setup
The platform is a university-focused archive and community tool built on top of Supabase — an open source Firebase alternative that exposes a REST API directly over your PostgreSQL database. It's a solid stack for moving fast. It also means that misconfigured Row Level Security (RLS) policies can turn your entire database into a public endpoint. Spoiler: some of them were misconfigured.
I opened Burp, set my proxy, and started clicking around. I hadn't finished my coffee before the first thing jumped out at me.
Finding #1 — "Faculty of Pwning"
Severity: Low / Funny
The registration endpoint looked standard enough:
POST /auth/v1/signupThe request body accepted a data object for user metadata — things like username, faculty, and study_year. Normal stuff. Except there was zero validation on any of these fields server-side.
I changed my faculty to "faculty of pwning" and my study year to "Anul 19". The server accepted it without complaint. HTTP 200. Account created. My profile now proudly displayed a faculty that does not exist and a study year that would make me approximately 40 years old.

This is a low-severity issue on its own — it's not going to get anyone's data stolen. But it's a signal. If the server isn't validating the easy stuff, what about the harder stuff?
I kept going.
Finding #2 — Dumping the Entire Database
Severity: Critical
Supabase exposes a PostgREST API at /rest/v1/. The way it works is simple: each table in your database becomes an endpoint. If RLS is not properly configured, those endpoints are wide open.
I started with the obvious:
GET /rest/v1/profiles?select=*Full user table. Every profile, every username, every point total, every faculty, every user ID. No authentication beyond the public anon key.

That was interesting. But I wanted to know how deep this went.
I asked the dev for the Supabase table names, since this was like a gray box pentest, just to check things faster I threw it into Burp Intruder, and ran a sniper attack against the /rest/v1/ endpoint — one payload per table name, watching the response sizes.
The results:

The archive_files table alone came back at 2,103,739 bytes — over 2MB of exam files, metadata, course names, teacher names, and file URLs, all readable without authentication.

The most alarming table was gemini_keys. It returned active API keys with usage counts. These were live credentials, sitting in a publicly accessible database table with no RLS protection. I flagged this immediately.

The messaging-related tables (chat_messages, chat_participants, chat_rooms) were technically accessible in terms of the endpoint responding, but I chose not to pull the actual content — that's private communication between users and goes beyond what's necessary to demonstrate the vulnerability.
Root cause: Row Level Security was either disabled or misconfigured on most tables. Supabase's anon key is intentionally public-facing, but it should only be able to read data that's explicitly allowed by RLS policies. Without those policies, the anon key becomes a skeleton key.
Finding #3 — Becoming Admin With One Request
Severity: High
The hint came from /auth/v1/user. The response included "is_admin": true inside user_metadata — a client-readable field. If the app was checking admin status from there, it was probably also setting it from there. Time to find out.
The way to achieve this was through the PATCH endpoint for updating your own profile:
PATCH /rest/v1/profiles?id=eq.<your-user-id>The filter was based on the id parameter in the URL. No server-side check to verify that the authenticated user actually owned that profile. The check, if any, was happening on the client.
I sent:
{
"points": 671337,
"is_admin": true
}Response: HTTP 204 No Content — success.

I refreshed my profile. I was now an admin with 671,337 points.
The is_admin flag should never be a field that a regular user can write to their own profile. This is a textbook broken access control vulnerability — the API trusted the client to only send what it was supposed to, rather than enforcing what the server would accept. The fix is straightforward: strip any privileged fields (is_admin, is_banned, role-related fields) from user-submitted PATCH requests before they hit the database, regardless of what the client sends.
On the admin panel side — there wasn't much functionality exposed to admins yet since the platform was early stage, so the practical blast radius here was mostly contained to the leaderboard. But the pattern is dangerous: if admin functionality expands later and this isn't fixed, the impact grows with it.
Finding #4 — Leaderboard Domination
Severity: Medium / Very Satisfying
This one follows directly from Finding #3. Since I could set my own points to any value, I set my main account to 999,999,999 points and watched it rocket to #1 on the university leaderboard. I put my alt account at 671,337 for a comfortable #2.
The result was a podium that looked like this:

This isn't just cosmetic. A leaderboard that can be manipulated this easily is worthless as a trust signal. If the platform ever ties leaderboard position to real benefits — scholarships, recognition, access (it probably won't), then this becomes a much more serious issue.
What I Didn't Find
I also spent time testing the JWTs used for authentication. The tokens were properly signed with HS256, expiry times were reasonable, and I couldn't find any algorithm confusion, weak secret issues, or claims that could be manipulated to escalate privileges through the token itself. The auth layer was actually solid — which made the authorization layer failures all the more jarring by comparison. Secure tokens handed to an API that doesn't check what they're allowed to do is a common and painful pattern.
The database after patch:

Disclosure
This was a fully authorized test conducted at the explicit request of the platform developer, who is a friend and classmate. All findings were reported immediately after discovery. The platform was in early development, which explains the attack surface. The developer responded quickly and began patching.
No real user data was extracted or stored beyond what was necessary to confirm the vulnerability. Screenshots used in this writeup have been redacted to remove personally identifiable information of other users.