Skip to content
SafeFix
SafeFix

Demo app · this is sample data from our test fixtures

This is what a SafeFix report looks like.

A scan of demo-notes, a notes app we built with mistakes on purpose. No real customer data.

Grade D

3 critical problems. Fix them first.

Strangers could read and change this app’s data. Start with these.

Next.js, Supabase, Docker Compose, GitHub Actions · 30 checks · 6 findings · read-only scan

  • Critical

    3

  • High

    0

  • Medium

    3

  • Low

    0

Critical3

Fix these today.

Criticaldb.rls-disabled

Anyone can read your users table.

Row Level Security is off for public.profiles

Row Level Security (RLS) is turned off for this table. Anyone, even people who are not logged in, can read, add, change and delete every row through the API. They only need your anon key, and that key is public: it is inside your website code.

Where
public.profiles
Evidence
RLS: off. API access: anon: SELECT, INSERT, UPDATE, DELETE; authenticated: SELECT, INSERT, UPDATE, DELETE.

How to fix it

Turn on Row Level Security for this table. Then add policies: with RLS on and no policies, the API blocks all access to the table, so your app cannot use it until you add them.

SQL · review before you run it
alter table public.profiles enable row level security;-- Next: add policies. Example: each logged-in user can use only their own rows.-- create policy "Users can manage their own rows" on public.profiles--   for all to authenticated using ((select auth.uid()) = id) with check ((select auth.uid()) = id);

Learn more:supabase.com

Criticaldb.policy-always-true

Strangers can change or delete everyone’s notes.

The policy "Anyone can do anything" lets anyone add, change and delete any row of public.notes

The condition of this policy is always true, so it lets every row through. Anyone, even people who are not logged in, can add, change and delete any row in this table through the API. So strangers can change or delete your users' data. They can fill the table with fake or harmful rows.

Where
public.notes (policy "Anyone can do anything")
Evidence
PERMISSIVE FOR ALL TO public USING (true) WITH CHECK (true)

How to fix it

Replace this policy with one that lets logged-in users reach only their own rows (rows where user_id is their user id).

SQL · review before you run it
drop policy "Anyone can do anything" on public.notes;create policy "Users can manage their own rows" on public.notes  for all to authenticated  using ((select auth.uid()) = user_id)  with check ((select auth.uid()) = user_id);

Learn more:supabase.com

Criticalsecrets.client-exposed

Your master database key is visible to every visitor.

Supabase service_role key is exposed to the browser through NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY

The key is stored in NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY. Variables that start with NEXT_PUBLIC_ are put into the code that runs in the browser, so every visitor can see this key. This key skips all your Row Level Security rules. With it, someone can read, change or delete all data in your database.

Where
.env.local:2
Evidence
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY = ••••••••a9Qz (Supabase service_role key)

How to fix it

Rotate this key now, because it is already public. "Rotate" means: create a new secret key in the Supabase dashboard (Project Settings > API Keys) and turn off the old key. Then keep the new key in a server-only variable with a name like SUPABASE_SERVICE_ROLE_KEY (without NEXT_PUBLIC_), and use it only in server code or an edge function.

Learn more:supabase.com

Medium3

Check these, and fix the ones that apply.

Mediumstorage.public-bucket

Anyone with a link can download files from “avatars”.

The storage bucket "avatars" is public

Anyone who has the link to a file in this bucket can download it, without logging in. Policies do not protect downloads from a public bucket. This is fine for files that everyone may see, like profile pictures or product images. It is risky for private files, like invoices or ID documents.

Where
storage bucket "avatars"
Evidence
storage.buckets: id = 'avatars', public = true.

How to fix it

If this bucket holds private files, make it private. Then your app must show files with signed URLs (links that stop working after some time). You can also change this in the Supabase dashboard, in the bucket settings.

SQL · review before you run it
update storage.buckets set public = false where id = 'avatars';

Learn more:supabase.com

Mediumdocker.db-port-exposed

Your database port is open to the whole internet.

PostgreSQL port of "db" is open on all network interfaces

The service "db" in docker-compose.yml runs postgres:16 and publishes port "5432:5432". Docker then listens on every network interface of the machine. On a server, this opens your database to the internet, where bots try to log in all day. Docker also writes its own firewall rules, so a firewall like ufw does not block this port. This is fine on your own laptop.

Where
docker-compose.yml:7
Evidence
ports: "5432:5432"

How to fix it

Publish the port on 127.0.0.1 only, so only this machine can connect. If no program outside Docker needs the database, remove ports completely: containers in the same compose file still reach it by its service name.

Config · review before you run it
    ports:      - "127.0.0.1:5432:5432"

Learn more:docs.docker.com

Mediumci.unpinned-action

Your deploy can run someone else’s new code without warning.

Action tj-actions/changed-files@v45 is not pinned to a commit

This step uses tj-actions/changed-files@v45. The tag "v45" can be moved to different code at any time, by the owner or by someone who hacks their account. This happened in March 2025 to tj-actions/changed-files: its tags were moved to code that printed the secrets of thousands of projects into their logs. A full commit SHA can never change.

Where
.github/workflows/deploy.yml:12
Evidence
uses: tj-actions/changed-files@v45

How to fix it

Replace "v45" with the full 40-character commit SHA, and keep the version in a comment. To find the SHA, open https://github.com/tj-actions/changed-files/tags, open v45 and copy the full commit SHA. Or run: git ls-remote https://github.com/tj-actions/changed-files v45 (for a tag, use the line that ends with ^{} if there is one). Dependabot can then keep the pins up to date for you.

Config · review before you run it
uses: tj-actions/changed-files@<full-commit-sha> # v45

Learn more:docs.github.comnvd.nist.gov

What we did not check

A good grade does not mean an app is fully secure. Every report lists where SafeFix stops, so you know what to check yourself.

  • Firebase, MongoDB, MySQL and Terraform. SafeFix does not check them yet.
  • Your live website: HTTP headers, TLS certificates and cookies.
  • The logic of your own app code, such as which page each user may open.
  • The rows inside your tables. SafeFix reads structure and rules, never your data.
  • Whether a database function checks who is calling is a careful guess (a heuristic), not a proof.

Want this report for your app?

SafeFix is in early access. Join the list and we will email you when your first scan is ready.