Skip to content
RLS Radar

This report is from a fake app built into RLS Radar. Nothing here belongs to a customer. Use it to see the tone before you scan a real site.

What this scan can and can't see

This is a passive outside view. Every request was a GET an anonymous visitor could already make.

It does not log in, and it does not test POST requests, authorization, injection, business logic, or whether one customer can see another's data.

A high perimeter score is not a security guarantee. Broken auth, cross-tenant leaks, and injection need a code review or an authenticated test.

When a paid scan has a connected repository or zip, the code check reads those files. It does not run the app, and it does not test authorization.

The letter grade is not a security guarantee. An AI summary is included only when that feature is switched on, and it stops at the spend cap.

Perimeter report

Weekend Market (sample)

Perimeter score

0/100

the outside view already looks serious

Code grade

F

A secret is exposed to the client bundle.

This was a full passive scan. Requests were GET only. Database rows and secret values were not stored. Perimeter: 0/100 — the outside view already looks serious. 10 critical findings should be fixed before real users rely on this app.

This is a passive check of responses an anonymous visitor can already get. It is not a penetration test, it does not try exploits, and a high perimeter score is not a promise that the app is secure.

Changed since last scan

This is the first stored scan, so there is no earlier perimeter to compare. Monitoring is what fills this section: each weekly rescan lists a newly exposed file, a header that disappeared, or a key that was not in the last report.

Code

Committed secrets, env files, client-exposed keys, npm audit, and Supabase SQL. This section stays empty on a free perimeter scan.

  • HighC20

    A secret is committed in the repository

    .env includes OpenAI secret key. Values are redacted (sk-p…01).

    A key in git history can be cloned by anyone with repo access, and it often leaks further through forks and CI logs.

    - Delete the secret from the file and from git history if it was pushed.
    - Rotate the key.
    - Load it from the host's environment, not from a committed file.
  • CriticalC23

    A secret is exposed to the client bundle

    components/pay.ts includes Stripe secret key. Values are redacted (sk_l…01).

    A NEXT_PUBLIC_ or VITE_ variable, or a file the browser ships, contains a key that should stay on the server. Anyone who loads the site can read it.

    - Remove the secret from client code and from any NEXT_PUBLIC_ or VITE_ variable.
    - Rotate the key. Treat the old value as public.
    - Read server-only secrets from a normal environment variable that the bundler does not inline.
  • HighC21

    An env file is committed

    .env

    Env files hold the keys the app uses in production. Committing one publishes them to everyone who can read the repo.

    - Remove the env file from the repo and from history if it was pushed.
    - Keep a .env.example with empty values.
    - Ignore .env in git.
  • MediumC22

    Env files are not ignored by git

    A committed env file is in the tree and .gitignore does not ignore .env.

    Without an ignore rule, the next save of .env is one commit away from publishing every key.

    - Add a line with .env to .gitignore.
    - Keep sharing only .env.example.
  • HighC24

    A dependency has a known vulnerability

    minimist (high): Prototype pollution in nested merge

    npm audit matched a package in this repo to a published advisory. The app may be exploitable through that dependency.

    - Run npm audit in this repo and apply the fix it offers, or upgrade the named package.
    - Re-run the code check after the lockfile changes.
  • HighC25

    A table is created without row level security

    notes

    A Supabase table without row level security is readable and writable by anyone who has the anon key, and that key is meant to live in the browser.

    - Enable row level security on each listed table.
    - Add a policy that names who can read and write the rows.
    - Do not use a policy of true on a table that holds private data.

Fix these first

  • CriticalC03

    An environment file is publicly downloadable

    /.env returned an environment file. Key names: OPENAI_API_KEY, DATABASE_URL. Values were not stored.

    A public .env file is how secret keys, database URLs, and private configuration end up on the open web. Even if the values look like tests, assume they are live until you have rotated them.

    Remove the file from the web root and from any static export.
    Tell the host not to serve dotfiles.
    Rotate every secret that was in the file.
  • CriticalC02

    Anthropic secret key is in files the browser can download

    sk-a…01 in /assets/app.js

    Anyone who can load the site can copy this Anthropic secret key and use it from their own machine. Treat it as public. Rotate it, then remove it from client code.

    Remove the Anthropic secret key from client code, public env files, and git history if it was committed.
    Rotate the key in the provider dashboard. The old value is burned.
    Keep the new key in a server-only environment variable. Never prefix it with NEXT_PUBLIC_.
  • CriticalC02

    OpenAI secret key is in files the browser can download

    sk-p…01 in /assets/app.js; sk-p…01 in /.env

    Anyone who can load the site can copy this OpenAI secret key and use it from their own machine. Treat it as public. Rotate it, then remove it from client code.

    Remove the OpenAI secret key from client code, public env files, and git history if it was committed.
    Rotate the key in the provider dashboard. The old value is burned.
    Keep the new key in a server-only environment variable. Never prefix it with NEXT_PUBLIC_.
  • CriticalC02

    Stripe secret key is in files the browser can download

    sk_l…01 in /assets/app.js

    Anyone who can load the site can copy this Stripe secret key and use it from their own machine. Treat it as public. Rotate it, then remove it from client code.

    Remove the Stripe secret key from client code, public env files, and git history if it was committed.
    Rotate the key in the provider dashboard. The old value is burned.
    Keep the new key in a server-only environment variable. Never prefix it with NEXT_PUBLIC_.
  • CriticalC02

    AWS access key id is in files the browser can download

    AKIA…12 in /assets/app.js

    Anyone who can load the site can copy this AWS access key id and use it from their own machine. Treat it as public. Rotate it, then remove it from client code.

    Remove the AWS access key id from client code, public env files, and git history if it was committed.
    Rotate the key in the provider dashboard. The old value is burned.
    Keep the new key in a server-only environment variable. Never prefix it with NEXT_PUBLIC_.
  • CriticalC01

    Supabase sb_secret key is in files the browser can download

    sb_s…01 in /assets/app.js

    Anyone who can load the site can copy this Supabase sb_secret key and use it from their own machine. Treat it as public. Rotate it, then remove it from client code.

    Remove the Supabase sb_secret key from client code, public env files, and git history if it was committed.
    Rotate the key in the provider dashboard. The old value is burned.
    Keep the new key in a server-only environment variable. Never prefix it with NEXT_PUBLIC_.
  • CriticalC02

    GitHub personal access token is in files the browser can download

    ghp_…00 in /assets/app.js

    Anyone who can load the site can copy this GitHub personal access token and use it from their own machine. Treat it as public. Rotate it, then remove it from client code.

    Remove the GitHub personal access token from client code, public env files, and git history if it was committed.
    Rotate the key in the provider dashboard. The old value is burned.
    Keep the new key in a server-only environment variable. Never prefix it with NEXT_PUBLIC_.
  • CriticalC02

    private key is in files the browser can download

    -----BEGIN PRIVATE KEY----- (block redacted) in /assets/app.js

    Anyone who can load the site can copy this private key and use it from their own machine. Treat it as public. Rotate it, then remove it from client code.

    Remove the private key from client code, public env files, and git history if it was committed.
    Rotate the key in the provider dashboard. The old value is burned.
    Keep the new key in a server-only environment variable. Never prefix it with NEXT_PUBLIC_.
  • CriticalC01

    Supabase service_role key is in files the browser can download

    eyJh…re in /assets/app.js

    Anyone who can load the site can copy this Supabase service_role key and use it from their own machine. Treat it as public. Rotate it, then remove it from client code.

    Remove the Supabase service_role key from client code, public env files, and git history if it was committed.
    Rotate the key in the provider dashboard. The old value is burned.
    Keep the new key in a server-only environment variable. Never prefix it with NEXT_PUBLIC_.
  • CriticalC06

    Private-looking Supabase tables can be read without logging in

    Anonymous GET with the public anon key against demoabcdproject.supabase.co: users (contains rows: yes), orders (contains rows: yes). Row contents were not stored.

    The anon key is in your JavaScript on purpose. Anyone can copy it and ask PostgREST for rows. If these tables answer, Row Level Security is off or a policy allows everyone. Names like users, orders, and messages usually hold data that should not be public.

    In the Supabase SQL editor, enable RLS on each listed table.
    Add a policy that matches the real rule, for example a user can read only their own row.
    Do not add `using (true)` on these tables.
  • HighC11

    A JavaScript source map includes original source

    /assets/app.js.map returned a source map. File names: supabaseClient.ts, page.tsx. Source text was not stored.

    The map includes sourcesContent, so a visitor can read the original source, comments, and any secrets that were compiled in.

    Stop publishing .map files on the production host, or strip sourcesContent.
  • HighC04

    Git metadata is publicly downloadable

    /.git/config, /.git/HEAD returned git metadata. Remote URLs were not stored if they contained credentials.

    A public .git directory can reveal source code, commit history, and sometimes tokens embedded in remote URLs. That is more than a visitor should be able to download.

    Block `/.git` on the host. If a token was in the remote URL, rotate it.
  • HighC07

    Firebase Realtime Database can be read without logging in

    GET Realtime Database root returned a readable response (data was present). Contents were not stored.

    The web API key in your JavaScript is public by design. The rules behind it are what keep data private. An anonymous read that returns data means those rules are open.

    In the Firebase console, require authentication for reads of private collections. Do not leave the root readable.
  • HighC07

    Firebase Firestore can be read without logging in

    GET Firestore document list returned a readable response (data was present). Contents were not stored.

    The web API key in your JavaScript is public by design. The rules behind it are what keep data private. An anonymous read that returns data means those rules are open.

    In the Firebase console, require authentication for reads of private collections. Do not leave the root readable.
  • HighC08

    Admin or debug routes respond without a login

    GET /api/admin returned 200 with a non-HTML body; GET /api/admin/users returned 200 with a non-HTML body; GET /api/seed returned 200 with a non-HTML body. Bodies were not stored.

    A 200 from /api/admin or /api/seed with a data body usually means the route forgot to check who is calling. Visitors can hit it the same way this scan did: a plain GET with no cookie.

    Require a session or a server-only secret on every admin, seed, and debug route. Return 401 when the caller is anonymous.

Worth doing

  • MediumC05

    Some baseline security headers are missing

    The homepage response did not include: Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options or CSP frame-ancestors, Referrer-Policy, Permissions-Policy.

    Headers will not save an open database, but they are cheap insurance against content sniffing, clickjacking, and extra script sources. Missing CSP is the one worth doing properly.

    - **Content-Security-Policy.** No CSP. A leaked script or a third-party widget can run with the full page.
    - **Strict-Transport-Security.** No HSTS. Browsers can still be talked into the http version of the site.
    - **X-Content-Type-Options.** Missing nosniff. Browsers may guess a content type you did not intend.
    - **X-Frame-Options or CSP frame-ancestors.** The page can be framed by other sites (clickjacking).
    - **Referrer-Policy.** The browser will choose how much of the URL to send to other sites.
    - **Permissions-Policy.** Camera, microphone, and similar features are not explicitly limited.

Notes

  • InfoC10

    Browser keys that are public by design

    Supabase anon key eyJh…re in /assets/app.js; Stripe publishable key pk_l…01 in /assets/app.js; Firebase web API key AIza…01 in /assets/app.js

    These keys are supposed to ship in the front end. They are a problem only when the database or storage rules behind them allow more than you meant. Separate findings cover open tables and open Firebase reads. Do not rotate these just because they are visible.

    Leave publishable keys in the client. Spend the time on Row Level Security and Firebase rules instead.
  • InfoC08

    Some admin-looking paths returned the website HTML

    /admin returned HTML rather than a JSON API. This is often a single-page app answering every path.

    That pattern is usually harmless. It can still hide a real admin screen that renders after JavaScript loads. Open the URL while logged out and see whether private controls appear.

    If the page is only a sign-in form or the public marketing shell, no change is required. If it renders private data before login, require authentication before rendering it.

False positives and accepted risks

A scanner that only shouts will train you to ignore it. These are the patterns that often look scary and are not.

  • Accepted riskC10

    A SECURITY DEFINER view is readable — confirm that is intentional

    Anonymous read against demoabcdproject.supabase.co: public_directory (contains rows: yes). The API description mentions SECURITY DEFINER. Row contents were not stored.

    SECURITY DEFINER means the view runs with the owner’s privileges. Teams do this on purpose for a public directory that hides private columns. The same pattern leaks data if the view selects emails, tokens, or payments. This is not automatically bad, and it is not automatically fine.

    Open the view definition. If it only returns fields you mean to publish, keep it and write down why. If it returns private columns, replace it with a narrower view and enable RLS on the underlying table.
  • Likely false positiveC10

    PostGIS catalog tables are readable

    Anonymous read against demoabcdproject.supabase.co: spatial_ref_sys (contains rows: yes). Row contents were not stored.

    These tables ship with PostGIS. They store coordinate-system definitions such as WGS84, not your customers. Advisors flag them because Row Level Security is off. Turning them off or dropping PostGIS is the wrong fix.

    Leave spatial_ref_sys and the other PostGIS catalogs in place. In the Supabase advisor, mark the lint as an accepted risk if you use maps.
  • PostGIS catalogs such as spatial_ref_sys

    Installing PostGIS creates public reference tables (spatial_ref_sys, geometry_columns, geography_columns). They list map projections, not your users. Supabase’s own advisor often flags them. Do not uninstall PostGIS just to clear the warning. If a finding names one of these tables, treat it as a likely false positive unless you stored your own data there.

  • Intentional SECURITY DEFINER views

    A SECURITY DEFINER view runs with the owner’s rights on purpose. Teams use that for a public directory that hides private columns. It can also leak data if the view selects too much. Read the view before you drop it. If it only exposes fields you meant to publish, call it an accepted risk and write down why. If it exposes emails, auth data, or payments, lock it down.

  • Supabase anon keys and Firebase web API keys

    These keys are designed to live in the browser. Finding one is not, by itself, a leak. It becomes a problem when the database policies or security rules behind the key let strangers read or write private data. The anon key is the front door key. Row Level Security is the lock.

  • Stripe publishable keys

    Keys that start with pk_live_ or pk_test_ are meant to be public. Keys that start with sk_live_ or rk_live_ are secret. If a secret key is in the browser, rotate it. Do not rotate a publishable key just because a scanner saw it.

  • Policies that use USING (true) on purpose

    A marketing blog or a public product catalog sometimes has a read policy of USING (true). That is fine when the table is meant to be public. The same policy on users, orders, or messages is a data leak. The table name and what you put in it decide the severity, not the policy syntax alone.

How this scan worked

RLS Radar sent 27 GET requests with a polite user agent. It did not send POST, PUT, PATCH, or DELETE. Supabase checks were read-only PostgREST selects with limit 1. Firebase checks were unauthenticated reads. Row contents, secret values, and source-map text were not stored. A high score is not a certificate.

Requests made
  • GET 200 https://demo.rlsradar.test/
  • GET 200 https://demo.rlsradar.test/admin
  • GET 200 https://demo.rlsradar.test/api/admin
  • GET 200 https://demo.rlsradar.test/api/admin/users
  • GET 200 https://demo.rlsradar.test/api/seed
  • GET 404 https://demo.rlsradar.test/api/debug
  • GET 404 https://demo.rlsradar.test/api/internal
  • GET 404 https://demo.rlsradar.test/debug
  • GET 200 https://demo.rlsradar.test/assets/app.js
  • GET 200 https://demo.rlsradar.test/assets/app.js.map
  • GET 200 https://demo.rlsradar.test/.env
  • GET 404 https://demo.rlsradar.test/.env.local
  • GET 404 https://demo.rlsradar.test/.env.production
  • GET 200 https://demo.rlsradar.test/.git/config
  • GET 200 https://demo.rlsradar.test/.git/HEAD
  • GET 404 https://demo.rlsradar.test/config.json
  • GET 404 https://demo.rlsradar.test/wp-config.php
  • GET 404 https://demo.rlsradar.test/backup.sql
  • GET 404 https://demo.rlsradar.test/dump.sql
  • GET 200 https://demoabcdproject.supabase.co/rest/v1/
  • GET 200 https://demoabcdproject.supabase.co/rest/v1/users?select=*&limit=1
  • GET 200 https://demoabcdproject.supabase.co/rest/v1/orders?select=*&limit=1
  • GET 200 https://demoabcdproject.supabase.co/rest/v1/spatial_ref_sys?select=*&limit=1
  • GET 200 https://demoabcdproject.supabase.co/rest/v1/public_directory?select=*&limit=1
  • GET 404 https://demoabcdproject.supabase.co/auth/v1/settings
  • GET 200 https://demo-app-default-rtdb.firebaseio.com/.json?shallow=true
  • GET 200 https://firestore.googleapis.com/v1/projects/demo-app/databases/(default)/documents?pageSize=1