Sitemap
InfoSec Write-ups

A collection of write-ups from the best hackers in the world on topics ranging from bug bounties and CTFs to vulnhub machines, hardware challenges and real life encounters. Subscribe to our weekly newsletter for the coolest infosec updates: https://weekly.infosecwriteups.com/

How I Exposed an AI Company’s Finances

5 min readMay 12, 2026

--

Disclosure Notice: This vulnerability was responsibly disclosed to the affected company. The issue has been fully remediated (RLS enforced, credentials rotated). All sensitive identifiers — company name, database URLs, API keys, and exact financial figures — have been redacted. This post is published with the company’s acknowledgement.

Odilon Redon’s Dark Symbolism

The Hunter’s Mindset: The Assumption Gap

In the rush to deploy cutting-edge AI, security often rests on a single point of failure: configuration. Developers frequently assume that because a Database-as-a-Service (DBaaS) like Supabase provides a “public” anon key, the data behind it is automatically shielded.

As a Threat Researcher, my mindset is different:

“If a key is public, I must assume every table it can reach is public — until I prove otherwise.”

This is the story of how a routine frontend audit uncovered a critical Material Non-Public Information (MNPI) leak at a high-growth AI company.

Step 1: Reconnaissance & JavaScript Static Analysis

The hunt began on the frontend of the target. Modern web applications — especially those using no-code builders like Webflow — rely on “glue code”: custom JavaScript bundles injected to handle dynamic data rendering, such as populating pricing pages.

I opened Chrome DevTools and navigated to the Sources tab. I identified a production npm package responsible for the company’s pricing page logic. Using the browser’s Pretty Print feature, I de-obfuscated the bundle and ran a regex search for supabase.

Step 2: Credential Extraction

Within the bundle, I found the initialization of a Supabase client. While the anon key is intended to be client-side safe, its presence shifts 100% of the security burden onto Row Level Security (RLS).

The Discovery:

import { createClient } from '@supabase/supabase-js';
// Initialization discovered during static analysis
const SUPABASE_URL = "https://[REDACTED].supabase.co";
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...[REDACTED]";
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);

Step 3: The Logical Pivot — Investigative Enumeration

Having extracted a valid anon key, I had a working identity to present to the database. The question became: what could I reach with it?

My hypothesis: The engineering team likely enforced RLS on user-facing tables — the ones directly rendered in the UI. But internal “helper” tables — those used for caching pre-calculated metrics — are frequently an afterthought. Developers create them for performance reasons, never intending them to be API-accessible, and then forget to apply RLS during security reviews.

This is the Assumption Gap in practice.

Why arr_cache specifically? This required understanding how developers think, not just how code works. When a pricing page needs to display real-time business metrics (like growth rates), engineers rarely query raw transaction tables on every page load — that's expensive. Instead, they create a lightweight cache table that stores a pre-aggregated snapshot. The naming convention almost writes itself:

  • arr → Annual Recurring Revenue, the north-star SaaS metric
  • _cache → a pre-computed aggregate, refreshed on a schedule

This wasn’t a lucky guess. It was researcher intuition built on understanding developer architecture patterns. In Supabase’s PostgREST model, every table is automatically exposed as an API endpoint at /rest/v1/<table_name>. Finding a table name is finding an attack surface.

Get Ashish Bogati’s stories in your inbox

Join Medium for free to get updates from this writer.

I moved from the browser entirely to the terminal — bypassing all frontend UI filters and React logic — to query the PostgREST API directly.

The Researcher’s Logic:

Evidence Inference The “Aha!” Moment Leaked anon key in JS bundle This key has a database role attached to it I have an identity — now test what doors it opens Hardcoded Supabase URL The database is directly exposed, not proxied I can bypass the frontend entirely with cURL Pricing page renders growth metrics A cache table almost certainly exists behind this Target arr_cache — the canonical naming pattern

The Attack Command:

curl -X GET "https://[REDACTED].supabase.co/rest/v1/arr_cache?select=*" \
-H "apikey: [REDACTED_ANON_KEY]" \
-H "Authorization: Bearer [REDACTED_ANON_KEY]"

Step 4: The Impact

The server returned a 200 OK — bypassing all authentication. The payload exposed live, highly sensitive business metrics.

The Response (figures redacted):

[
{
"id": 1,
"current_arr": "[REDACTED — $XX.XM+]",
"pct_growth_30d": "[REDACTED — XX%]",
"updated_at": "2026-04-01T05:21:05.715Z"
}
]

For any company — especially an AI firm — real-time ARR and growth rate data represents board-level, investor-sensitive information. This is a textbook Broken Access Control (BAC) vulnerability leading to direct disclosure of Material Non-Public Information (MNPI).

Triage Note: The company’s security team triaged this report as Critical severity and moved to remediation within 24 hours — confirming that “guessable internal tables with missing RLS” is a threat they take seriously, not a theoretical edge case. The speed of the fix is itself evidence of impact.

Press enter or click to view image in full size
bounty reward

Step 5: Remediation & Verification

Upon responsible disclosure, the security team acknowledged the severity immediately. Because the anon key had been exposed with broad permissions, the fix required two actions:

  1. Global Key Rotation — Invalidating the compromised key across all environments.
  2. RLS Enforcement — Applying a strict DENY ALL policy to the arr_cache table and auditing all internal tables.

Verification (24 hours later):

curl -X GET "https://[REDACTED].supabase.co/rest/v1/arr_cache?select=*" \
-H "apikey: [ROTATED_KEY]"
{ "message": "Invalid API key" }

Conclusion: Lessons for the AI Era

Lesson Takeaway Client-Side is Public Space If a key is in your JS bundle, treat it as a fully public credential. RLS is Not Optional In Supabase, every table should have a DENY ALL policy by default — including internal cache tables. Audit Your "Glue" Code Custom scripts connecting third-party tools (Webflow, Framer, etc.) are often the weakest link in your security chain. Naming Conventions Are Signals Tables named arr_cache, internal_metrics, or admin_data are prime targets for enumeration.

About the Author

Ashish Bogati is a Threat Researcher and Security Auditor specializing in cloud architecture, modern web infrastructure, and responsible disclosure.

Tags: #ThreatResearch #BugBounty #Supabase #CyberSecurity #InfoSec #ResponsibleDisclosure #BrokenAccessControl #MNPI

--

InfoSec Write-ups
InfoSec Write-ups

Published in InfoSec Write-ups

A collection of write-ups from the best hackers in the world on topics ranging from bug bounties and CTFs to vulnhub machines, hardware challenges and real life encounters. Subscribe to our weekly newsletter for the coolest infosec updates: https://weekly.infosecwriteups.com/