| name | flutter-supabase-security-audit |
| description | Security audit for Flutter/Dart + Supabase projects. Use this skill when you need a full-repo security scan (frontend + Supabase backend) with verified findings (low false positives) and a Markdown report under reports/security/. Do NOT use for regular feature development or general code review.
|
Flutter + Supabase Security Audit (Markdown Report)
Goal
- Find realistically exploitable security vulnerabilities (not just style/best-practice issues).
- Actively verify each finding (collect evidence AND attempt to refute).
- Write a Markdown Report as a file in the repo at the end.
- Default mode: Report-only (no code changes). Changes only if the user explicitly requests "apply fixes" / "patch".
Inputs (optional, parsed from user message)
- scope:
full (default) | frontend | backend
- output: Path to report file (default see below)
- mode:
report (default) | apply (only when explicitly requested)
- max_findings: default 50
If nothing is specified: scope=full, mode=report, max_findings=50.
Default Output Paths
- Report file:
reports/security/security-audit-YYYY-MM-DD-hhmmss.md
- Additionally (optional):
reports/security/latest.md as a copy of the same content.
- For Supabase remote checks, store additional JSON artifacts under
reports/security/:
phase2_remote_summary.json
phase2_supabase_db_lint_summary.json
- optional raw data (
phase2_supabase_db_lint_linked_clean.json, etc.)
If the directory does not exist: create it.
Security/Compliance Rules for the Scan
- No exploitable payloads or step-by-step attack instructions.
- Never output secrets in the report. Always redact: e.g.
sb_secret_abc…(redacted) or prefix + hash only.
- No new production dependencies as a fix without clear justification + explicit approval.
- Only list findings as "verified" when you have concrete code/policy evidence.
Uncertain items go into "Needs manual verification".
Workflow
Phase 0 — Repo Inventory (quick, structured)
- Identify project structure:
- Flutter:
pubspec.yaml, lib/, android/, ios/, web/ (if present)
- Supabase:
supabase/ (migrations, seed, functions, config.toml, etc.)
- Identify auth flow:
- Where is Supabase Auth initialized?
- Where are sessions/tokens stored?
- Identify data entry points:
- Deep links / dynamic links
- Push-notification payloads
- User input → DB writes/updates
- File uploads (Supabase Storage)
- Define scope excludes (do not scan unless relevant):
- Build outputs (
build/, .dart_tool/, ios/Pods/, android/.gradle/, etc.)
- Generated files
Phase A — Discovery Scan (broad)
A1: Secrets & Key Handling
- Search for Supabase keys / tokens / secrets:
service_role, sb_secret_, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_SECRET, JWT_SECRET, private keys
- Check
.env*, *.secrets*, config.*, CI configs.
- Ensure: The Flutter client may contain at most the anon/publishable key — never service_role/secret key.
- Documentation Hygiene: Flag all secrets, including sample/local/demo tokens in markdown files (e.g.
README.md), as Low severity findings. Normalizing secret leakage in docs is a security anti-pattern even if the keys are loopback/dev keys.
A2: Flutter/Dart Security Hotspots
Search for:
- Insecure TLS / Certificate Bypass:
HttpOverrides, badCertificateCallback, custom SecurityContext without pinning/checks
- Token/Sensitive Storage:
- Usage of
shared_preferences, hive, sqflite, plain files for tokens/PII (check instead for secure storage providers, e.g. flutter_secure_storage)
- Logging of tokens/headers (Note:
print, debugPrint, and developer.log() are prohibited in this repo. Explicitly check for PII/secrets leak risk via the central LogService)
- Offline Data & Sync:
- Is cached offline data containing PII (personally identifiable information) stored unencrypted in local databases (e.g. Drift/SQLite)?
- Verification path: Check whether Drift/SQLite databases are created with encryption (e.g. via
sqflite_sqlcipher, encrypted_moor, or sqlcipher_flutter_libs). Search for encryption key setup in the DB initialization. If no encryption library is present in pubspec.yaml and no encryption password is passed when opening the DB → unencrypted. Document whether PII-containing tables are affected by identifying columns/tables with names, emails, phone numbers, addresses, or health data.
- Key management: Even when encryption is present, verify whether the encryption key is securely managed. A hardcoded key in source code, a key in
shared_preferences, or a key transmitted over insecure channels provides no real protection. The key must originate from a secure store (e.g. flutter_secure_storage, Android Keystore, iOS Keychain). If encryption is present but the key is insecure → classify as Medium finding.
- URL Handling / Deep Links (App Links / Universal Links):
Uri.parse(...) on untrusted input
url_launcher / WebViews with untrusted URLs
- Missing domain ownership verification (e.g. check for
assetlinks.json or apple-app-site-association to protect against link spoofing)
- Supply Chain & Vulnerable Dependencies:
- Check for outdated dependencies via
flutter pub outdated (shows version status only, not CVE hits).
- Security Advisories (gated check):
- Availability check: Run
dart pub --help and check the output for the word advisories. If advisories appears in the subcommand list → proceed to step 2. If not → go directly to step 3. Important: Do not use dart pub advisories --help, as some SDK versions return generic help with exit code 0 even though the subcommand does not exist.
- Run
dart pub advisories and document results.
- Fallback (always when step 2 is unavailable): Check at minimum all direct dependencies flagged as outdated by
flutter pub outdated, plus any dependencies in security-critical areas (auth, crypto, network), against osv.dev (Ecosystem: Pub, enter package name). Document results in the report.
- In the report section "Commands & Outputs", document which path (step 2 or 3) was actually used. Unavailability of
dart pub advisories is not an error, but expected behavior in many SDK versions.
- Do not claim verified CVE hits when only version information is available.
- Input → DB writes:
- Validation/normalization before
insert/update (even when DB has RLS)
A3: Supabase Backend Security Hotspots
RLS / Data API Exposure
-
Check SQL migrations and/or schema definitions:
- Is RLS enabled per table/view/function?
- Are there policies for SELECT/INSERT/UPDATE/DELETE?
- Are there suspiciously permissive policies (e.g.
USING (true) or TO anon without restrictions)?
-
RLS Performance as Security Vector: Check whether policies use auth.uid() directly instead of (SELECT auth.uid()). Direct usage forces volatile per-row evaluation, which can lead to performance issues up to DoS on large tables (see supabase_performance.md). Severity calibration: Without concrete evidence for table size (>100k rows) or load patterns, classify as Low. Only upgrade to Medium or higher with demonstrated size/load evidence.
-
Missing FK Indexes: Check whether all foreign key columns have an index. PostgreSQL does not create these automatically. Missing FK indexes can be exploited as a DoS vector through slow JOINs/lookups. Severity calibration: Default classification is Low. Upgrade to Medium only with demonstrably large tables or frequently used JOINs. Check via:
SELECT conrelid::regclass AS table_name,
a.attname AS fk_column,
NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = conrelid
AND a.attnum = ANY(i.indkey)
) AS missing_index
FROM pg_constraint c
JOIN pg_attribute a ON a.attrelid = conrelid AND a.attnum = ANY(c.conkey)
WHERE c.contype = 'f';
SECURITY DEFINER Privilege Escalation
- Check all
SECURITY DEFINER functions specifically:
- Can an
anon or authenticated user call a SECURITY DEFINER function that returns data that RLS would normally block?
- Is
search_path explicitly set in SECURITY DEFINER functions? (Without an explicit search_path, an attacker can manipulate schema-qualified calls → privilege escalation).
- Does the function contain dynamic SQL (
EXECUTE, EXECUTE format(...)) with user input? → SQL injection risk despite RLS.
- Check
GRANT EXECUTE on these functions: Who is allowed to call them?
Storage
- Check policies on
storage.objects.
- Check buckets: private/public, and whether private buckets are correctly protected via policies.
Realtime (Presence & Broadcasts)
- Check Realtime channels for unauthorized reading/listening.
- Sending of sensitive data via Broadcast that could be intercepted without RLS/policy checks.
- Extended Realtime checks:
- Are
postgres_changes subscriptions correctly restricted to authorized schemas/tables? A channel listening on a table without RLS exposes all changes to every connected client.
- Is
Broadcast used for sensitive data (PII, tokens, internal IDs) without server-side payload validation? Broadcasts bypass RLS entirely.
- Is
Presence used in channels that synchronize sensitive user metadata (e.g. GPS position, status, device information) that should not be accessible to all channel participants?
Edge Functions (if present)
- Check
supabase/config.toml:
verify_jwt = false only for genuine webhooks/public endpoints; in that case, there must be an alternative auth/signature verification.
- CRITICAL: Do NOT discard a finding simply because
verify_jwt = true. A valid JWT only proves who the user is (Authentication), it does not authorize what the client is doing on their behalf.
- Privilege Escalation: Check if the Edge Function uses a
service_role client. If yes, trace ALL data inputs (request body, URL params, and especially data fetched from DB rows that the user can UPDATE).
- Path-Injection/ID-Manipulation: Ensure the user cannot manipulate a path, ID, or file name to delete or access files/records outside their permission boundaries.
- Check that service_role/secret key is only used server-side (Deno env), never hardcoded.
Migration Safety (optional, supplementary)
- Check SQL migration files for destructive DDL statements that violate
permanent_data_integrity.md:
DROP TABLE, DROP COLUMN, ALTER COLUMN ... TYPE on populated columns
DELETE without WHERE clause or with very broad filter
TRUNCATE
- Check for dynamic SQL (
EXECUTE, EXECUTE format(...)) in migration files — potential injection risk if variables are not properly escaped.
- Check filename convention: Files must follow
YYYYMMDDHHMMSS_name.sql. Non-conforming filenames are skipped by Supabase tooling and could cause schema drift.
A4: Native Platform Security Hotspots
- Android (
android/app/src/main/AndroidManifest.xml):
- Check for insecure configurations:
android:usesCleartextTraffic="true" (should be false).
- Overly permissive exported components (
android:exported="true" without specific justification).
- iOS (
ios/Runner/Info.plist):
NSAppTransportSecurity / NSAllowsArbitraryLoads (should be avoided).
- Missing or unspecific permission descriptions (privacy strings).
- Web (
web/index.html and hosting setup):
- Missing basic CSP headers (Content Security Policy) in hosting setup.
- Extended Web Security Headers:
X-Frame-Options (clickjacking protection) — should be DENY or SAMEORIGIN.
X-Content-Type-Options: nosniff (prevents MIME type sniffing).
Referrer-Policy (controls referrer information to third parties).
Permissions-Policy (restricts browser features like geolocation, camera).
- CORS configuration: Check Supabase-side CORS settings and Edge Function CORS headers for overly permissive origins (
* instead of specific domains).
- Evidence rule for deployment configuration: CSP, security headers, and CORS can only be classified as "verified" if either (a) the hosting configuration is in the repo (e.g.
firebase.json, vercel.json, netlify.toml, Nginx config) and is auditable there, or (b) real remote evidence via HTTP response headers is available. If neither repo config nor remote access is available → classify directly as "Needs manual verification", not as a verified finding.
Phase B — Verification (False-Positive Filter)
Completeness Constraint: You MUST explicitly evaluate every single bullet point from Phase A1, A2, A3, and A4. Do not stop your scan early just because you found critical vulnerabilities. The audit is evaluating the entire attack surface.
For each potential finding:
- Evidence:
- Specific file + line(s)
- Data flow: source (untrusted) → sink (DB write / privileged op)
- For Supabase: affected table/policy/role
- Refutation attempt:
- Is there already an RLS/policy that prevents it?
- Is it only active in debug builds?
- Is it unreachable code?
Only if still plausible/real after B2: include as a finding.
Phase C — Tooling/Remote Checks (MCP-first, CLI fallback)
C1: Local App Checks (best effort)
- Flutter:
dart analyze or flutter analyze
flutter test — run if fast (<2 min); primarily to confirm no regressions exist; not a security check per se
flutter pub outdated (for dependency checks)
dart pub advisories — only if available (see gated check in A2), otherwise osv.dev fallback
C2: Supabase Remote Checks (agent-agnostic)
This phase defines what must be checked, not with which specific tool. The concrete provisioning of the MCP server varies by agent environment (e.g. tool prefix, configuration method); the query intents remain identical.
Access path hierarchy (obligatory in this order):
- MCP Server (primary, obligatory when available): Use the Supabase MCP server as provisioned in the respective agent environment. Execute the required queries below as MCP tool calls.
- Supabase CLI (fallback):
supabase CLI, locally linked, only when no MCP server is available.
- Direct SQL queries (fallback): Via any SQL executor against the database.
- Supabase Dashboard (last resort): Manual, only when no programmatic access is possible.
If no remote access is available: document in the report and perform static analysis of local supabase/ files only.
Required queries (via the available access path):
| Query Intent | Purpose | Example Tool (MCP) | Fallback (CLI/SQL) |
|---|
| Security Advisors | Known security warnings | get_advisors(type=security) | Dashboard → Advisors |
| Performance Advisors | Performance warnings with security relevance | get_advisors(type=performance) | Dashboard → Advisors |
| Tables & RLS Status | RLS activation, policy completeness | list_tables(verbose=true) | SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public'; |
| Edge Functions | JWT verify, secrets handling | list_edge_functions + get_edge_function | supabase functions list |
| Policy Details | Permissive policies, auth.uid() usage | execute_sql on pg_policies | SELECT * FROM pg_policies; |
| SECURITY DEFINER | Privilege escalation, search_path | execute_sql on pg_proc | SQL query from A3 |
| FK Indexes | Missing indexes as DoS vector | execute_sql with FK query | SQL query from A3 |
| Extensions | Check activated extensions | list_extensions | SELECT extname, extversion FROM pg_extension; |
Write a concise JSON summary to reports/security/phase2_remote_summary.json (regardless of the access path used).
C3: Supabase CLI Lint (additional or fallback)
- If CLI is available and project is linked:
supabase db lint --linked --output json
- Summarize findings structurally in
reports/security/phase2_supabase_db_lint_summary.json.
- If only raw JSON is available, optionally store as
phase2_supabase_db_lint_linked_clean.json.
If commands fail: document in the report (reason + what was checked statically instead).
Phase D — Write Report (Markdown file)
Create/overwrite the report file (default or output=).
Use the template at templates/report_template.md as the foundation to ensure consistent report structure across audit runs.
Report Structure (mandatory)
- Title + metadata:
- Date, scope, mode, commit hash
- Skill version (file date or hash of SKILL.md)
- Flutter/Dart SDK version (
flutter --version)
- Supabase CLI version (
supabase --version, if available)
- Remote access path used (MCP / CLI / SQL / Dashboard / static-only)
- Architecture & Trust Boundaries (Flutter ↔ Supabase)
- Findings (max_findings, sorted by severity):
- De-duplication: Multiple symptoms of the same root cause must be merged into one finding. E.g. if 5 tables have the same RLS issue → 1 finding listing all affected tables, not 5 separate findings. This keeps
max_findings meaningful and reduces noise.
- For each finding:
- Title
- Severity: Critical/High/Medium/Low
- Confidence: 0.0–1.0
- Component: Frontend | Supabase DB | Storage | Edge Functions | Realtime | Platform Config
- Affected locations: file:line
- Evidence: verified path, why exploitable
- Evidence basis:
live remote | linked CLI | static repo | manual dashboard (identifies the verification data source)
- Recommendation
- Patch (unified diff) or "No safe auto-fix"
- How to verify (concrete steps/commands)
- Needs manual verification (if applicable)
- Discarded candidates (3–5 examples + why discarded)
- Commands & outputs (abbreviated, relevant snippets), incl. remote check evidence + artifact paths
- mode=apply only — DoD evidence (mandatory per
definition_of_done.md):
- Changed/new test files (paths)
- Scenario mapping: which findings covered by which tests (happy/error/edge)
- Verification evidence: output of
scripts/verify_dod.sh or scripts/run_local_ci.sh
- For migration fixes:
supabase db reset --local --yes + supabase db diff --linked --schema public result
- Explicit completion declaration:
Status: Completed or Status: Not completed
Severity Guidelines
- Critical: Data exfiltration / account takeover / RCE / privilege escalation with low effort
- High: Sensitive data exposure or authz bypass, but with constraints
- Medium: Viable impact, but more difficult / context-dependent
- Low: Hardening / defense-in-depth / best practice
Default: No Code Changes
- When mode=report: No production code files are modified. Only report and JSON artifacts are written under
reports/security/.
- When mode=apply: The skill is primarily report-first by design.
mode=apply is an add-on mode that enables changes but triggers the full repo workflow:
- Changes require explicit user approval.
- Auto-commits are strictly prohibited (
git_workflow.md).
- Fixes must be verified via the defined CI/DB entry points (e.g.
scripts/verify_dod.sh or scripts/run_local_ci.sh) per definition_of_done.md requirements.
- The report must fully populate the DoD evidence section (report structure item 7).
- Without complete DoD evidence, the report must not carry
Status: Completed.