Security Lab

Watch the defenses work

Three hands-on demos. Each one lets you play the attacker, then shows exactly how Relay stops it — using the same checks that run in the real product. Everything here runs live in your browser on synthetic data.

Demo 1

Tenant isolation

Relay is multi-tenant. Signed in for one client, try to read another client's rows — Postgres Row-Level Security decides what actually comes back.

In plain English

Relay keeps every client's data in one shared database — like one apartment building full of tenants. Each client must only ever see their own rows.

You try: log in as someone at Brightside Dental, then ask the database for a different company's (Apex Law's) records.
If it's blocked: the database itself refuses and hands back nothing at all — not even an error — so an attacker can't even tell the other company exists.
Signed in asalice@brightside-dental.com
Read rows for:
select * from ghl_opportunities
where client_id = 'a1a1a1a1-…-apexlaw';
Request
Authenticated as Brightside
Supabase anon key
Postgres · RLS policy
accessible_client_ids() →
{ b2b2…-brightside }
a1a1a1a1… ∈ set ?
Result

Takeaway — Tenancy is enforced by an RLS policy in the database — client_id IN (accessible_client_ids()) — so it runs before any application code. A cross-tenant read isn't an error, it's silently zero rows, and even a leaked anon key can only read what the signed-in user is allowed to.

Demo 2

OAuth state forgery

Connecting a social account round-trips through OAuth. The state is signed so an attacker can't forge a callback that repoints a grant — or links their own account — to someone else's dashboard.

In plain English

Connecting a social account is like a coat check: Relay hands you a ticket on the way out to Facebook and checks it on the way back. The ticket carries a wax-seal signature only Relay's server can make.

You try: grab a real ticket and change which client it's for — but you can't re-create the seal without the server's secret.
If it's blocked: Relay re-checks the seal against your tampered ticket, sees it no longer matches, and rejects it before anything gets connected.
Attacker move:
Incoming callback
GET /api/auth/meta/callback?code=&state=computing…
↑ clientId tampered to another tenant — but the attacker can't re-sign it without the server secret.
Signing key: demo-meta-app-secret (server-only · never shipped to the browser)

Takeaway — The state is HMAC-SHA256 signed with a server-only secret and verified in constant time, behind a 10-minute TTL. Tamper one byte of the payload and the recomputed signature no longer matches; forgery and replay both fail before any token is exchanged.

Demo 3

SSRF guard

Reports embed the client's logo from a client-supplied URL. Point it at an internal address and the egress guard has to stop the server from ever fetching it.

In plain English

Reports show the client's logo from a URL someone typed in, so Relay's server goes and fetches it. But the server sits inside the private network and can reach things outsiders can't — including the cloud's secret-keys service (the mistake behind the Capital One breach).

You try: set the logo URL to an internal address (like the cloud-metadata server) to make Relay's server fetch something it shouldn't.
If it's blocked: a guard checks the address first and refuses anything pointing inward, so the server never touches internal secrets.
Set the client logo URL to:
http://169.254.169.254/latest/meta-data/
Report renderer
inlineImage(url)
SSRF guard · isBlockedHost()
protocol ∈ {http, https}
resolve → 169.254.169.254
Result

Takeaway — Before fetching, the guard enforces an http/https whitelist and blocks loopback, link-local / cloud-metadata, and private IP ranges — and only forwards the session cookie to our own origin. The headless renderer can't be turned into a window into internal infrastructure.

Every check above runs live in your browser on synthetic data — no real accounts, tokens, or tenants are involved. The production implementations live in the app's source.

vladimircuc.com