A static application security testing (SAST) toolkit for Flutter + Supabase projects. The repository ships two separate scanners that share the same finding codes but run different engines:
| Component | Location | Engine | When to use it |
|---|---|---|---|
| VS Code extension | vscode-extension/ |
Regex + tree-sitter AST + JS/TS taint tracking, with optional Rust engine-cli sidecar rules for Dart taint flows |
Day-to-day authoring — inline quick fixes, AST/REGEX badges, taint-confirmed findings |
| Dart CLI | bin/fluttersupabasehelper.dart |
Rust engine-cli sidecar when installed, plus regex/structural Flutter + Supabase rules with committed SQL DDL awareness |
CI gating on Flutter apps, headless scans, local dart run |
The Dart CLI remains useful without engine-cli: when the Rust binary is
missing it falls back to its built-in structural rules and prints one install
hint. Rust-backed IFDS taint findings are only produced when engine-cli is
available through ENGINE_CLI, a bundled binary, or a global install.
Current shipped features: SARIF 2.1.0 output, standalone HTML reports, CWE taxonomy, data-flow paths in findings, AST-aware inline suppressions, content-hash baselines, git diff mode for PR-style scans, SARIF diff mode, confidence-based CI failure, parallel rule execution, multi-root workspaces, on-save scanning, per-rule disable, statement-aware suppression boundary, optional Rust-backed Dart taint rules, and regression-tested taint engine behavior.
What It Detects
- Injection flaws — SQL injection, command injection, code execution,
XSS, path traversal, SSRF, open-redirect, LDAP/XPath, NoSQL
$where, template injection, Python format-string injection, deserialization (source-to-sink taint confirmed where applicable) - Hardcoded secrets — AWS keys, private keys, JWT tokens, hardcoded Supabase anon keys, high-entropy strings
- JWT misuse —
jwt.decodewithout verify,algorithms: ['none'], hardcoded HMAC secrets (incl. file-local variable tracking), HS* verifier configuration with public-key-looking values - Insecure cookies —
httpOnly: false,secure: false,sameSite: 'none'without secure - CORS misconfiguration — wildcard origin + credentials,
cors({ origin: true, credentials: true }), origin reflection - Insecure randomness —
Math.random()for security-sensitive values - JavaScript platform risks — cleartext HTTP clients, tabnabbing, broad cookie domains, sensitive web storage, clipboard exposure, symlink-following filesystem reads, deprecated TLS protocol pinning, weak crypto APIs, typosquatted packages, prototype-pollution writes, high-confidence ReDoS regex patterns
- Supabase misconfigurations — missing table/operation-level RLS DDL,
policies without enabled RLS, insecure storage rules,
committed
.envfiles, public buckets, unscoped realtime channels, leaked realtime subscriptions, multiple clients, improper init - Mobile/WebView risks — Android
addJavascriptInterfacebridges andflutter_secure_storagevalues written to logs - Unsafe patterns — sensitive logging, debug artifacts, client-side trust violations, missing upload validation
Every security finding carries a CWE identifier (e.g. CWE-89 SQL injection,
CWE-78 command, CWE-918 SSRF, CWE-79 XSS, CWE-338 insecure random,
CWE-347 JWT, CWE-942 CORS, CWE-798 hardcoded credentials).
Supported Languages (VS Code extension)
| Tier | Languages | Analysis |
|---|---|---|
| Full | JavaScript, TypeScript, JSX, TSX | Regex + AST + intra-procedural taint tracking |
| Full (Rust sidecar when installed) | Dart | Regex + AST heuristics + Rust engine-cli Semgrep-style taint rules |
| AST | Python, Go, Java | Regex + AST structural patterns |
| Regex | Kotlin, SQL, YAML, JSON, .env |
Heuristic rules only |
Analysis Pipeline (VS Code extension)
- Regex — fast heuristic pass across all files
- AST — selective parsing via
web-tree-sitterWASM grammars - Taint tracking — intra-procedural data-flow analysis with provenance capture (JS/TS), plus optional Rust-backed Dart taint rules
Rules within each stage run in parallel (Promise.all); stages
themselves run sequentially because later stages depend on caches built by
earlier ones.
Taint Model
The taint tracker performs intra-procedural source-to-sink analysis with the following properties:
- Sources:
- Strong (always tainted):
req/requestparameters, parameters literally nameduserInput,req.body/query/params/headers/...member access,process.env/stdin,platform.environment, Flask/Django/FastAPI request fields, controller.text/.value.URLSearchParams.get(...)is treated as user-controlled query input. - Heuristic (tainted only with handler-shape sibling):
data,payload,input— flagged as a source only when the function also accepts ares/response/next/reply/ctxparameter (Express handler shape).
- Strong (always tainted):
- Sinks (with CWE):
- SQL (
query,execute,raw,rawQuery, …) —CWE-89. Receiver must be DB-shaped (db,database,client,pool,prismaClient,myDb,getDb(),pgPool, …) OR the first argument must contain a SQL keyword string. Bareanalytics.query()is not flagged. - Command —
exec,spawn,subprocess.run,os.system—CWE-78. - Code —
eval,Function,setTimeout/setInterval(string arg),pickle.loads,yaml.load,marshal.loads—CWE-95. - HTML —
innerHTML,outerHTML,document.write,dangerouslySetInnerHTML,res.send/response.send/reply.sendresponse-body sinks —CWE-79. - URL/SSRF —
fetch,axios.*,http.get,requests.*,urllib.urlopen—CWE-918. - Path —
fs.readFile/writeFile/...—CWE-22. - Redirect —
res.redirect,res.location—CWE-601. - NoSQL — Mongo
find/update/etc. with$where/$function/$accumulatoroperators —CWE-943. - Template/SSTI —
render_template_string, Jinjafrom_string—CWE-1336. - LDAP/XPath —
ldap.search,xpath.evaluate—CWE-89family.
- SQL (
- Sanitizers (per-sink kind — §QW-1 / §PR-6):
- Full-spectrum (clear taint for every sink kind):
- Numeric coercion:
parseInt,parseFloat,Number.parseInt,Number.parseFloat,Number, unary+,~~,| 0,>>> 0. Return type is a number — cannot carry payloads. - Validators:
validate/assertValid/ensureSafe/schema.parse/safeParse/.... - Generic catch-all:
sanitize/clean/normalize/escapewhen no more specific name matches. - Allowlist-stripping replace:
.replace(/[^A-Za-z0-9_-]/g, ''),.replace(/\D/g, ''), etc.
- Numeric coercion:
- Sink-specific (only suppress matching sink kinds):
- HTML:
escapeHtml,encodeHTML,sanitizeHtml,dompurify.sanitize,validator.escape. - URL / redirect / header:
encodeURI,encodeURIComponent. - SQL:
escapeSql,sqlstring.escape,mysql.escape,pg.escape. - Command:
escapeShell,shellEscape.
- HTML:
escapeHtml(taint)flowing into a SQL sink still flags HIGH;sqlstring.escape(taint)into a SQL sink correctly suppresses.- Parameterized queries recognized at the call level (second arg is
[…]/{…}/(…)or matchesparams/values/bindings/parameters).
- Full-spectrum (clear taint for every sink kind):
- Precision controls:
- Clean reassignment (
x = 42) clears taint; augmented assignment (x += value) preserves it. - Conditional sanitization (
if (cond) x = sanitize(...)) does not promote a variable to sanitized — the engine refuses to trust a sanitization that may not execute on every path. - Negate-guard recognition (§QW-41 / §PR-1):
if (!ALLOW.has(x)) return;(and.includes,.test,.indexOf(...) === -1,!ALLOW[x]shapes with earlyreturn/throw/continue/break) clears taint onxfor code past the guard. instanceofreceiver narrowing (§QW-2 / §EN-12): inside anif (handle instanceof Pool) { ... }branch the SQL receiver check treatshandleas DB-shaped ifPool/PrismaClient/Sequelize/Database/ etc. tokenize to a known DB keyword.- Alias chains beyond depth 3 degrade to weak taint (reported at MEDIUM confidence).
Object.assign(target, src)markstargetas weakly tainted — property accesses liketarget.timeoutare not flagged.- Function summaries: same-file functions with
return <direct-source>propagate taint through call sites. - Class receiver flow: tainted
this.Xprops from a constructor are visible to sibling methods at MEDIUM confidence.
- Clean reassignment (
Provenance / data-flow paths
When a taint finding is produced, the engine captures the symbol chain that connects the source to the sink. This appears in:
- JSON output —
pathSteps: [{line, column, label, filePath}, …]. - SARIF output — emitted as
codeFlows(compatible with GitHub Code Scanning's data-flow viewer). - VS Code panel — rendered under each finding as an ordered list.
Example chain for db.query("SELECT … " + sql) where sql = "…" + id,
id = req.body.id:
1. line 1 — tainted source: 'req' parameter (assumed user-controlled)
2. line 2 — propagated: id = req.body.id
3. line 3 — propagated: sql = "SELECT … " + id
4. line 4 — sink: db.query
Rust-Backed Dart Taint
When engine-cli is available, Dart projects get an additional Rust-backed
taint pass through the rust-engine-taint rule. The extension and Dart CLI
invoke the Rust binary with Semgrep-style YAML rules for Dart SQL injection,
command injection, and HTML rendering/XSS sinks, then merge the resulting
findings into normal scanner output.
- The Rust workspace builds and its direct SQL/command smoke flows are regression-tested.
- The Rust kernel is still experimental. PDG-aware variable tracking and some Dart syntax coverage are incomplete, so expect false negatives on flows that require deeper Dart semantic reconstruction.
- If the binary is missing, the scanner falls back to the built-in rules rather than failing the whole scan.
The existing intra-procedural InjectionRule still runs; Rust-backed Dart
taint complements it. Duplicate findings on the same line are de-duped by the
scanner.
Confidence Levels
| Confidence | Source | Typical FP Rate |
|---|---|---|
| HIGH | Confirmed taint flow to sink | <5% |
| MEDIUM | Structural AST pattern or indirect taint | 10–20% |
| LOW | Regex / entropy heuristic | Higher — informational |
CLI Usage
Dart CLI (regex-only, ships from the repo root)
The Dart CLI is the recommended path for scanning a Flutter + Supabase app from CI or a terminal without Node.js. It loads the project, runs the rule set in lib/src/rules/, and prints human-readable findings.
dart run fluttersupabasehelper # scan the current directory
dart run fluttersupabasehelper ./my-project # scan a specific path
dart run fluttersupabasehelper --no-suggestions
Exit code 1 when any non-suggestion finding is reported, 0 otherwise — drop it straight into CI.
The Dart CLI does not perform AST parsing or taint tracking. It uses targeted regular expressions plus a small amount of statement-level context (nearest .from(...) call, nearby filter methods, comment-line heuristics). That keeps it fast (< 1 s on a typical Flutter repo) and hermetic, at the cost of missing data-flow vulnerabilities that only surface across multiple statements. For those, use the VS Code extension.
Node CLI (ships with the VS Code extension)
npx flutter-supabase-security-scanner scan ./my-project
npx flutter-supabase-security-scanner scan . --pretty # human-readable
npx flutter-supabase-security-scanner scan . --json # machine-readable (default)
npx flutter-supabase-security-scanner scan . --summary # counts only
npx flutter-supabase-security-scanner scan . --sarif -o sast.sarif # SARIF 2.1.0
Output Formats
| Flag | Format | Use |
|---|---|---|
--json (default) |
Custom JSON, includes pathSteps, cwe, full ranges |
Machine consumption |
--pretty |
Grouped human report with severity icons | Terminal |
--summary |
Counts only | Quick CI sanity check |
--sarif |
SARIF 2.1.0 with codeFlows, per-rule CWE taxonomy, partialFingerprints |
GitHub Code Scanning, GitLab, Azure DevOps |
--markdown |
Markdown table | PR comments and issues |
--csv |
RFC 4180 CSV | Spreadsheets and BI tools |
--junit |
JUnit XML | Jenkins, CircleCI, Buildkite |
--gitlab |
GitLab Code Quality JSON | GitLab merge request widgets |
--bitbucket |
Bitbucket Code Insights annotations | Bitbucket pull requests |
--html |
Self-contained HTML with severity/rule/file filters | CI artifacts and shareable reports |
CI Integration
# GitHub Actions — fail build + upload findings to Code Scanning
- name: SAST scan
run: npx flutter-supabase-security-scanner scan . --sarif -o sast.sarif --fail-on high
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: sast.sarif }
--fail-on <level> exits with code 1 when findings at or above the level
exist (high|medium|low).
--fail-confidence <level> adds a confidence gate. For example,
--fail-on high --fail-confidence high fails only on HIGH-severity,
HIGH-confidence findings, while still reporting lower-confidence results.
PR-style scans (--changed-since)
npx flutter-supabase-security-scanner scan . --changed-since main --sarif -o pr.sarif
Restricts findings to files modified since the given git ref (uses git diff --name-only <ref>...HEAD plus git status --porcelain for uncommitted
work). Combine with --fail-on high for low-friction PR gating.
SARIF diff mode (--diff-against)
npx flutter-supabase-security-scanner scan . --sarif -o current.sarif
npx flutter-supabase-security-scanner scan . --json --diff-against previous.sarif
Filters out findings whose SARIF partial fingerprint already appeared in an older scan. This is useful for PR comments that should show only newly introduced findings, while a full SARIF upload remains available for code scanning dashboards.
pre-commit
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: flutter-supabase-security-scanner-sast
name: Flutter Supabase Security Scanner SAST
entry: npx flutter-supabase-security-scanner scan . --changed-since HEAD --fail-on high --fail-confidence high --summary
language: system
pass_filenames: false
pass_filenames: false is intentional: the scanner already computes changed
files via git, which keeps path handling consistent with CI.
Husky + lint-staged
Same intent as pre-commit, expressed in the npm-native toolchain. After
npx husky init, drop this in .husky/pre-commit:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
…and configure lint-staged in package.json:
{
"lint-staged": {
// Run the SAST scan once per commit, ignoring filenames — the scanner
// computes changed files itself and bails early when none are touched.
// The trailing `[]` makes lint-staged skip its default per-file fan-out.
"*": "bash -c 'npx flutter-supabase-security-scanner scan . --changed-since HEAD --fail-on high --fail-confidence high --summary' --"
}
}
Why --changed-since HEAD: lint-staged is invoked once per commit, but the
scanner is a project-wide tool. Letting it ask git for the staged diff
matches both the pre-commit and CI flows above and keeps the answer to
"what changed?" in one place.
Baseline (suppress known issues)
npx flutter-supabase-security-scanner baseline . # snapshot current findings (v2 with content hashes)
npx flutter-supabase-security-scanner scan . --baseline # report only new findings
Baseline saved to .sast-baseline.json. v2 baselines store a content hash
of the ±2 lines around each finding so the match survives line shifts from
unrelated edits. v1 baselines (older format) still load — match falls back
to fingerprint.
Per-rule disable
# CLI: comma-separated, repeatable
npx flutter-supabase-security-scanner scan . --disable high-entropy-secret,file-upload-validation
Unknown rule codes produce a warning listing all valid codes (so typos surface immediately).
// .vscode/settings.json
{
"flutterSupabaseSecurityScanner.disabledRules": ["high-entropy-secret"],
"flutterSupabaseSecurityScanner.scanOnSave": false
}
Validation Harness
npx flutter-supabase-security-scanner validate ./test_repos
Scans subdirectories and outputs per-rule precision statistics and confidence distribution.
Suppression
Inline (statement-aware)
// sast-ignore-next-line
const secret = "AKIA…";
// sast-ignore injection-flaw
db.query(
"SELECT * FROM users WHERE id = " + id, // multi-line wraps still suppressed
);
// Single comment ≠ wholesale silence:
// sast-ignore injection-flaw
db.query("ok" + req.body.id);
db.query("FN" + req.body.id); // NOT suppressed — separate statement
// Rust-engine taint can also be suppressed by code:
// sast-ignore rust-engine-taint
db.rawQuery(userInput);
The window is brace/paren-balanced and terminates at the first ; or },
so multi-line wraps work but two unrelated statements after one comment do
not get suppressed together.
.sastignore
vendor/
*.min.js
fixtures
config/seeds.js
.gitignore
The scanner also respects the root-level .gitignore during file traversal —
no need to duplicate node_modules/, dist/, generated/, etc. in
.sastignore.
VS Code Extension
Activates automatically on project open.
| Command | Description |
|---|---|
Flutter Supabase Security Scanner: Scan Workspace |
Full project scan (multi-root aware) |
Flutter Supabase Security Scanner: Scan Active File's Project |
Scan from active file |
On-save scanning (off by default) — set
flutterSupabaseSecurityScanner.scanOnSave: true in your settings to re-run the
scan 750 ms after any supported file is saved.
Quick fixes include real autofixes (not just TODO comments):
Math.random()→crypto.randomUUID()orcrypto.randomBytes(16).toString('hex')algorithms: ['none']→algorithms: ['HS256']httpOnly: false→httpOnly: truesecure: false→secure: true.innerHTML =→.textContent =- Simple
db.query("…" + ident)→db.query("…?", [ident]) - Plus suppression comments for any finding
Results panel shows findings grouped by file or severity, clickable
file:line locations, AST/REGEX badges, CWE links to mitre.org, full
data-flow paths, scan stats, and an AST health warning if the parse failure
rate exceeds 20%. The webview is constructed via DOM API (zero innerHTML)
under a strict CSP (script-src 'nonce-…').
Performance
- Files > 1 MB skipped
.gitignorehonored during traversal (root-level)node_modules,dist,build,.git, etc. always excluded- AST parsers cached per language; WASM grammars loaded lazily
- Rules within a stage execute concurrently via
Promise.all - Lock files, minified files, and docs excluded from entropy scanning
- Typically <100 ms for projects under 200 files
Telemetry
Local-only. Stored under your platform's data directory:
| Platform | Path |
|---|---|
| macOS | ~/Library/Application Support/flutter-supabase-security-scanner/telemetry.json |
| Linux | $XDG_DATA_HOME/flutter-supabase-security-scanner/telemetry.json (or ~/.local/share/...) |
| Windows | %LOCALAPPDATA%/flutter-supabase-security-scanner/telemetry.json |
Env-driven paths are validated to live under your home directory; suspicious values fall through to a safer default.
Limitations
- JS/TS taint is mostly intra-procedural. It has limited same-file helper summaries, but no whole-program or cross-file flow.
- Dart Rust-engine taint is optional at runtime and still experimental.
It does not yet model every Dart language feature (
await, cascade operators, collection sensitivity, full virtual dispatch, implicitthis, etc.). - Python, Go, Java have AST grammars but limited source/sink coverage and no taint models.
- Non-Dart languages: no full CFG-based path-sensitive analysis. Top-level
negate-guard barriers and dominating
instanceofnarrowing are recognized via AST pre-passes (§QW-41 / §QW-2); positiveif (allow.has(x)) sink(x)guards and nested-block guards still defer to the §EN-4 work. Conditional reassignment to a sanitizer is conservatively not trusted, which biases toward false positives over false negatives. - Framework modeling is still shallow. Express-style request fields and common response-body sinks are modeled, but route registration, middleware chains, and framework-specific lifecycle rules are not complete.
- Entropy detection is probabilistic — some benign strings flagged at LOW.
- Comment-based suppression can be added by anyone with commit access; CI policies should review/restrict suppression patterns.
Building from source
This repo ships three independent components — each can be built standalone:
1. Rust analysis kernel (engine/)
The high-precision IFDS taint solver. It is currently experimental: the Rust workspace builds and tests pass, and direct SQL/command taint smoke flows work. PDG-aware variable tracking and some Dart syntax coverage, such as named constructor arguments, are still in progress.
# One-shot setup (detects + installs missing tools, with confirmation prompts)
./scripts/bootstrap.sh
# Or manually:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.85.0
source $HOME/.cargo/env
cd engine && cargo build --workspace --release
The binary lands at engine/target/release/engine-cli. Resolver helpers for
the TS extension and Dart lib/ scanner live at
vscode-extension/src/scanner/engineResolver.ts
and lib/src/engine/engine_resolver.dart;
TS sidecar integration and Dart lib/ sidecar integration are tracked in
RUST_ENGINE_PROGRESS.md.
The intended runtime behavior is a precise install hint when the binary is missing, rather than a silent failure.
For build-from-source contributors: read BLUEPRINT_RUST_ENGINE.md end-to-end before touching engine/. Track progress in RUST_ENGINE_PROGRESS.md.
2. TypeScript VS Code extension (vscode-extension/)
cd vscode-extension
npm install
npm run compile
Release builds produced by .github/workflows/release-binaries.yml stage
prebuilt engine-cli binaries for the supported targets before packaging.
Contributors can also build the engine from source when testing Rust-backed
analysis locally.
3. Dart lib/ scanner (lib/)
dart pub get
dart run bin/fluttersupabasehelper.dart .
Tests
npm run compile && npm test
Runs in order:
precision-self-test.js— end-to-end fixture sweep covering 17 rule interactions across JS / Dart / Python / test-path noise.taint-engine.test.js— unit-style invariants for theIntraProceduralTaintTracker(source seeding, sink-specific sanitizer recognition, receiver heuristic incl. instanceof narrowing, sink kinds, reassignment, parameterization, negate-guard, all CWE-tagged rule quick wins, comment immunity, git porcelain regression,Promise.allconcurrency stability).test-ast.js— AST grammar load smoke test.- Rust engine smoke tests live under the Rust workflow and verify direct
Dart taint flows through
engine-cli.
Requirements
[email protected] paired with [email protected] (ABI 13).