How I Exposed an AI Company’s Finances
Disclosure Notice: This vulnerability was responsibly disclosed to the affected company. The issue h 2026-5-31 00:50:51 Author: infosecwriteups.com(查看原文) 阅读量:13 收藏

Ashish Bogati

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.

Remember me for faster sign in

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


文章来源: https://infosecwriteups.com/how-i-exposed-an-ai-companys-finances-d1e162a3b996?source=rss----7b722bfd1b8d--bug_bounty
如有侵权请联系:admin#unsafe.sh