Skip to main content
Surface local security candidates for verification. Two rule families ship: the graph-structural client-server-leak (a "use client" file that directly reads, or transitively imports a module that reads, a non-public process.env secret), and the data-driven tainted-sink catalogue (syntactic dangerous-sink candidates across a catalogue of CWE categories). Both default to off and run only under fallow security.
Findings are candidates, not confirmed vulnerabilities. Fallow reports a structural trace so an agent or human can verify whether the value can actually reach client-bundled code.
fallow security

Options

Output

FlagDescription
-f, --format <FORMAT>Output format: human (default), json, or sarif
-q, --quietSuppress progress output
--summaryShow a compact human summary instead of per-finding detail
--ciCI mode: equivalent to --format sarif --fail-on-issues --quiet
--fail-on-issuesExit with code 1 if security candidates are found
--sarif-file <PATH>Write SARIF output to a file in addition to the primary output
--legacy-envelopeEmit JSON without the top-level kind discriminator for one migration cycle

Scoping

FlagDescription
-r, --root <PATH>Project root directory (default: current working directory)
-c, --config <PATH>Path to config file (default: auto-detected)
--changed-since <REF> (alias: --base)Only report candidates whose client anchor or trace hops touch files changed since a git ref
--file <PATH>Only report candidates whose finding anchor or trace hop matches the selected file. Repeat to select multiple files. The full graph is still analyzed
--diff-file <PATH>Narrow candidates to added hunks on the client anchor or import trace. Secret-source hops use file-level retention because member-access spans are not yet stored. Use - to read from stdin.
--diff-stdinRead the unified diff from stdin
-w, --workspace <PATTERNS>Scope output to selected workspace packages
--changed-workspaces <REF>Scope output to workspace packages touched since the given git ref

Performance

FlagDescription
--no-cacheDisable incremental caching
--threads <N>Number of parser threads

Regression gate

FlagDescription
--gate newFail (exit code 8) only when the change introduces a NEW security-sink candidate in the changed lines, not on the whole candidate backlog. Requires a diff source (--changed-since, --diff-file, or --diff-stdin).
The gate is the first-line-of-defence form of fallow security: a refactor that merely touches a file already containing a sink passes, while a change that adds a new sink (or wires a new untrusted source into an existing sink) on a changed line fails. Findings stay unverified candidates: the human output says REVIEW REQUIRED (not FAIL), SARIF keeps every result at level: note with the verdict in run.properties.fallowGate, and --format json carries an additive gate block (mode / verdict / new_count). Exit codes: 8 = a new candidate was introduced in the changed lines; 0 = clean (or a docs-only / empty diff); 2 = the gate could not compute the diff (an unfetched ref on a shallow clone, a bad ref, not a git repo). Exit 8 is dedicated and stable, so a pipeline can soft-gate it without allow-listing real errors (GitLab allow_failure: exit_codes: [8]).
On a shallow clone the merge-base may not be fetched. In GitHub Actions set fetch-depth: 0 on actions/checkout; in GitLab CI set GIT_DEPTH: 0. The gate exits 2 (loud) rather than passing silently when it cannot compute the diff.
CI gate on changed
# GitHub Action / generic CI: gate the PR's committed range
fallow security --gate new --changed-since "$BASE_SHA"

# Pre-commit hook: gate the STAGED content (not committed HEAD)
git diff --cached --unified=0 | fallow security --gate new --diff-stdin

Rule: client-server-leak

The detector starts at files with a top-level "use client" directive and walks static imports. It reports a candidate when the client boundary can reach a module that reads a non-public process.env value. Public-by-convention env values are excluded:
Public prefixExample
NODE_ENVprocess.env.NODE_ENV
NEXT_PUBLIC_*process.env.NEXT_PUBLIC_API_URL
VITE_*process.env.VITE_API_URL
NUXT_PUBLIC_*process.env.NUXT_PUBLIC_SITE_URL
REACT_APP_*process.env.REACT_APP_API_URL
PUBLIC_*process.env.PUBLIC_SITE_URL
GATSBY_*process.env.GATSBY_SITE_URL
EXPO_PUBLIC_*process.env.EXPO_PUBLIC_API_URL
STORYBOOK_*process.env.STORYBOOK_THEME
Dynamic import() edges that the graph cannot follow are counted in the output as unresolved edge files. A clean finding list with a non-zero unresolved count is not a clean bill.

Rule: tainted-sink (catalogue)

A data-driven catalogue of syntactic sink candidates. Where client-server-leak is a graph-reachability rule, tainted-sink flags a call, member assignment, or tagged template that reaches a known dangerous sink. Most rows require a non-literal argument; narrowly literal-aware rows flag deterministic unsafe literals such as wildcard postMessage origins, weak crypto algorithms, disabled TLS validation, and JWT algorithm issues. All catalogue findings carry kind: "tainted-sink" plus a category (the catalogue id) and a cwe number. The catalogue ships these categories:
CategoryCWESink shape
dangerous-html79innerHTML / outerHTML / insertAdjacentHTML / dangerouslySetInnerHTML
template-escape-bypass79template-engine SafeString(...) wrapping a non-literal value
command-injection78child_process exec / execSync / spawn / spawnSync (import-provenance gated)
code-injection94eval / vm.runInNewContext
dynamic-regex1333RegExp(...) / new RegExp(...) with a non-literal pattern
redos-regex1333vulnerable regex literals tested with source-backed input
resource-amplification400source-backed size into Array(...) / new Array(...) / Buffer.alloc* / String.prototype.repeat / padStart / padEnd (directly Math.min-clamped sizes stay quiet)
dynamic-module-load95dynamic require(...)
sql-injection89query / execute with concatenation or interpolation, raw escape hatches (sql.raw, Prisma unsafe raw, Knex raw, sequelize.literal)
ssrf918fetch / got / ky / needle / request / axios / superagent / undici / http(s).request
path-traversal22path.join / path.resolve / node:fs path methods / route sendFile
header-injection113response setHeader / writeHead
open-redirect601res.redirect / location.href / location.assign / window.open
postmessage-wildcard-origin346postMessage(..., "*")
tls-validation-disabled295HTTPS/TLS options with rejectUnauthorized: false, plus NODE_TLS_REJECT_UNAUTHORIZED = "0"
cleartext-transport319cleartext http:// URLs in fetch-like calls and WebSocket constructors
electron-unsafe-webpreferences1188Electron webPreferences with unsafe literal options
world-writable-permission732chmod / chmodSync with world-writable modes
insecure-temp-file377predictable temporary file paths in fs writes
mysql-multiple-statements89MySQL connection options with multipleStatements: true
permissive-cors942CORS wildcard origin with credentials
insecure-cookie614cookie options missing or disabling httpOnly / secure
mass-assignment915source-backed Object.assign(target, source)
weak-crypto327runtime-selectable hash or cipher algorithm
deprecated-cipher327crypto.createCipher / createDecipher (no IV, MD5-based KDF)
insecure-randomness338crypto.pseudoRandomBytes(...)
unsafe-buffer-alloc1188Buffer.allocUnsafe / allocUnsafeSlow (uninitialized memory)
unsafe-deserialization502js-yaml load / node-serialize
prototype-pollution1321__proto__ writes and recursive merge sources
zip-slip22archive extraction destination paths
nosql-injection943Mongo / Mongoose query object passthrough
ssti1336template engine compile / render calls
xxe611XML parse calls
secret-pii-log532source-backed secrets or request PII reaching logs
hardcoded-secret798provider-prefix credentials and high-entropy literals assigned to secret-shaped identifiers (include-required)
xpath-injection643xpath.select / select1 with a non-literal expression
jwt-alg-none347JWT signing with algorithm none
jwt-verify-missing-algorithms347jsonwebtoken verify calls missing an algorithms allowlist
webview-injection94react-native-webview injectJavaScript(...) / injectedJavaScript= (enabler-gated)
angular-trusted-html79Angular bypassSecurityTrust* (enabler-gated)
nextjs-open-redirect601Next.js redirect / permanentRedirect (enabler-gated)
dom-document-write79document.write / document.writeln
jquery-html79jQuery .html(value) (enabler-gated)
route-send-file22Express / Fastify / Hono route sendFile (enabler-gated)
These are deliberately conservative candidates: a non-literal argument is a signal to verify, not proof of a vulnerability. Fallow does not prove the value is attacker-controlled or reaches the sink unsanitized. Verification is the agent’s job.
Sink-shaped nodes whose callee cannot be resolved to a static path (dynamic dispatch, computed members, aliased bindings) are counted in the output as unresolved_callee_sites. As with client-server-leak, a clean finding list with a non-zero count is not a clean bill.

Enabling categories

tainted-sink, hardcoded-secret, and client-server-leak default to off and are surfaced only by fallow security (never under bare fallow or the audit gate). Scope which catalogue categories run with security.categories in config:
{
  "security": {
    "categories": {
      "include": ["dangerous-html", "command-injection", "hardcoded-secret"],
      "exclude": []
    }
  }
}
With both lists empty, ordinary catalogue categories are active. hardcoded-secret is intentionally include-required and only runs when listed in security.categories.include.

Suppression

Suppress a known false positive at file level. Each rule has its own token:
// fallow-ignore-file security-client-server-leak
"use client";
// fallow-ignore-file security-sink
const el = document.querySelector(".out");
el.innerHTML = render(userInput);
One security-sink token covers every catalogue category. Use suppression only after verifying that the value cannot reach the sink unsanitized, for example because the input is a trusted constant, server-only, or sanitized upstream.

JSON output

--format json emits a typed root envelope with kind: "security" unless --legacy-envelope is set.
{
  "kind": "security",
  "schema_version": "1",
  "security_findings": [],
  "unresolved_edge_files": 0,
  "unresolved_callee_sites": 0
}
Each finding includes kind, path, line, col, evidence, trace, actions, and optional reachability. tainted-sink findings additionally carry category (the catalogue id, for example "dangerous-html") and cwe (the category’s CWE number); client-server-leak findings omit both. tainted-sink findings can also include reachability.untrusted_source_trace when a module with a known untrusted source imports the sink module. It is ranking and triage context only, not proof that a specific value reaches the sink.

Examples

fallow security