Notely (fictional app)
AI + human review · Next.js + Supabase · Commit 4f2a9c1
Summary
- critical2
- high2
- medium1
- low1
Fix these 3 first
- Enable Row Level Security on the notes table (F1).
- Move the service_role key to the server and rotate it (F2).
- Check ownership in the notes API route (F3).
Findings
Row Level Security disabled on the “notes” table
supabase/migrations/003_notes.sql:1
- What’s wrong
- The notes table was created without Row Level Security. Supabase exposes every table through its API, and your public key is in the browser.
- Concrete risk
- Anyone can read, edit or delete every user’s notes with a single request, without logging in.
In supabase/migrations/003_notes.sql, the table "notes" has Row Level Security disabled. Create a new migration that enables RLS on this table and adds policies so users can only select, insert, update and delete their own rows (auth.uid() = user_id). Do not change any other table.
Supabase service_role key used in client-side code
src/lib/supabaseAdmin.ts:4
- What’s wrong
- The service_role key bypasses every security rule. It is imported in a file that ends up in the browser bundle.
- Concrete risk
- Anyone who opens the browser developer tools gets full admin access to your database.
In src/lib/supabaseAdmin.ts, the Supabase service_role key is used in code that is bundled for the browser. Move every use of this client to server-only code (route handlers or server actions), read the key from a server environment variable without the NEXT_PUBLIC_ prefix, and use the anon key in the browser. Then tell me to rotate the service_role key in the Supabase dashboard.
API route returns any user’s data without checking who is asking
src/app/api/notes/[id]/route.ts:12
- What’s wrong
- The route loads a note by the ID in the URL but never checks that it belongs to the logged-in user.
- Concrete risk
- A logged-in user can read other users’ notes by changing the ID in the URL.
In src/app/api/notes/[id]/route.ts, the GET handler returns a note by id without checking ownership. Get the current user from the Supabase session, return 401 if there is none, and only return the note if note.user_id equals the user's id (404 otherwise). Do not change the response format.
Vulnerable dependency: outdated version of next
package.json:14
- What’s wrong
- The installed version of next has publicly known security advisories that are fixed in later releases.
- Concrete risk
- Known flaws are the first thing automated attacks look for.
In package.json, upgrade next to the latest patch release of the major version currently used, run the install, and fix any breaking changes reported by the build. Do not upgrade other packages.
No Content-Security-Policy header
next.config.ts
- What’s wrong
- Without a Content-Security-Policy, the browser will run any script injected into your pages.
- Concrete risk
- Turns a small injection bug into account takeover.
In next.config.ts, add security headers for all routes: a Content-Security-Policy that only allows scripts from 'self' and the Supabase project URL, plus Referrer-Policy: strict-origin-when-cross-origin and X-Content-Type-Options: nosniff. List the domains you allowed so I can check them.
Source maps published in production
next.config.ts
- What’s wrong
- Production source maps let anyone read your original source code.
- Concrete risk
- Makes it easier for an attacker to find other weaknesses.
In next.config.ts, make sure productionBrowserSourceMaps is false (or removed) so source maps are not published in production. Do not change anything else.
What was checked
- Secrets and keys in the code and the Git history
- Row Level Security on every table
- Authentication and ownership checks on API routes
- Known vulnerabilities in dependencies
- Security headers and production build settings
What was not checked
- Business logic not visible in the code (third-party dashboards, manual processes)
- Infrastructure and hosting configuration
- Behaviour of the live app under attack (no penetration testing)
Engineer’s recommendations
Included from $249- Add a check to your deployment that fails if any public table has RLS disabled.
- Keep every privileged Supabase call in one server-only module so the key can’t leak again.
- Two false positives from the automated pass were removed after review.
Action plan
Fix F1 and F2 today, F3 and F4 this week, then F5 and F6. When you’re done, a re-scan gives you a before/after report with both scores side by side.
