Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
512 changes: 464 additions & 48 deletions .github/workflows/security.yml

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ fix.md
issue*.md
pr*.md
ISSUE_*.md
PR_*.md
PR_*.md

# Security scanner artifacts written into the workspace by the
# Security Checks workflow (.github/workflows/security.yml)
trufflehog-results.json
semgrep.sarif
243 changes: 243 additions & 0 deletions .semgrep/flowfi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# FlowFi Semgrep rules.
#
# Rules in this file are *merge-blocking*: an `error`-level finding from a
# `flowfi.*` rule fails the Security Gate and blocks the pull request (see the
# "Evaluate FlowFi SAST gate" step in .github/workflows/security.yml).
#
# Because of that, every rule here is deliberately narrow and high precision.
# A rule that fires on existing, reviewed code is a bug in the rule, not a
# useful signal — widen it deliberately rather than silencing the gate.
#
# Broad, upstream coverage lives in Semgrep's `p/default` ruleset, which is
# reported to the Security tab for triage but does not block merges.
#
# Run locally:
# semgrep scan --config .semgrep/flowfi.yml --metrics=off backend frontend contracts
#
# Test a change to this file without touching the gate:
# semgrep scan --config .semgrep/flowfi.yml --test .semgrep

rules:
# -------------------------------------------------------------------
# Credentials
# -------------------------------------------------------------------
- id: flowfi.stellar-secret-key
message: >-
A Stellar secret key (the `S...` form) is committed. This is the key that
can sign transactions and move funds. Revoke it on the network, then purge
it from git history.
severity: ERROR
languages: [generic]
paths:
include:
- '**/*.ts'
- '**/*.tsx'
- '**/*.js'
- '**/*.mjs'
- '**/*.rs'
- '**/*.json'
- '**/*.toml'
- '**/*.sh'
- '**/*.yml'
- '**/*.yaml'
# Stellar secret seeds are strkey-encoded: an `S` version byte followed by
# exactly 56 base32 characters (1 version + 32 payload + 2 CRC = 35 bytes =
# 280 bits / 5). The exact length is what keeps this from firing on
# ordinary uppercase identifiers, and it also naturally excludes the
# deliberately malformed key fixtures in the frontend validation tests —
# so test paths are scanned like any other, rather than allowlisted.
pattern-regex: '\bS[A-Z2-7]{56}\b'

- id: flowfi.private-key-material
message: >-
Private key material is committed. Remove it, rotate the key, and purge it
from git history. Load key material from the environment instead.
severity: ERROR
languages: [generic]
paths:
include:
- '**/*.ts'
- '**/*.tsx'
- '**/*.js'
- '**/*.mjs'
- '**/*.rs'
- '**/*.json'
- '**/*.sh'
- '**/*.yml'
- '**/*.yaml'
pattern-regex: '-----BEGIN (RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----'

- id: flowfi.provider-api-token
message: >-
A provider API token is hardcoded. Read it from the environment
(`process.env.*`) or the GitHub Actions secrets store, then rotate the
exposed token.
severity: ERROR
languages: [generic]
paths:
include:
- '**/*.ts'
- '**/*.tsx'
- '**/*.js'
- '**/*.mjs'
- '**/*.rs'
- '**/*.json'
- '**/*.sh'
- '**/*.yml'
- '**/*.yaml'
# Recognisable prefixes for tokens that are live credentials on their own.
# Deliberately matches prefixes rather than "any long string near the word
# secret" — entropy heuristics fire on every base64 blob in the tree.
patterns:
- pattern-regex: '\b(gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,}|npm_[A-Za-z0-9]{30,}|sk_live_[A-Za-z0-9]{16,})\b'
- pattern-not-regex: '(?i)\b(example|placeholder|dummy|fake|sample|redacted|xxxx+|<[^>]+>|\$\{|\{\{)'

# -------------------------------------------------------------------
# Injection
# -------------------------------------------------------------------
- id: flowfi.prisma-raw-sql-interpolation
message: >-
`$queryRawUnsafe`/`$executeRawUnsafe` is being called with an interpolated
string. Build the query with positional placeholders (`$1`, `$2`, …) and
pass the values as separate arguments, or use `Prisma.sql`, so user input
can never become SQL syntax.
severity: ERROR
languages: [typescript, javascript]
paths:
include:
- 'backend/src/**'
- 'frontend/src/**'
# OR, not AND: a regex rule and structural rules under `patterns:` would be
# intersected, and no single call site can match both shapes.
pattern-either:
# A template literal passed to an *unsafe* Prisma call that contains a
# `${...}` hole. The reviewed calls in stream.controller.ts and
# withdraw.ts use a static template with `$1`/`$2`/`$3` placeholders and
# no interpolation — those are the pattern this rule steers people
# towards, so it must not match them. A regex is used rather than a
# structural pattern because the call and its template argument are
# routinely split across lines.
- pattern-regex: '(?s)\$(?:queryRawUnsafe|executeRawUnsafe)\(\s*`[^`]*\$\{'
# String concatenation is the same injection shape.
- pattern: $DB.$queryRawUnsafe('...' + $X)
- pattern: $DB.$executeRawUnsafe('...' + $X)

- id: flowfi.node-shell-injection
message: >-
A shell command is built from a non-literal argument. Pass an argument
vector with `execFile`/`spawn` (no shell) instead of interpolating values
into a shell string.
severity: ERROR
languages: [typescript, javascript]
paths:
include:
- 'backend/src/**'
- 'frontend/src/**'
- 'scripts/**'
# Anchored to an actual `child_process` import. A bare `$CP.exec(...)`
# pattern also matches `RegExp.prototype.exec`, which is everywhere.
patterns:
- pattern-either:
- pattern: |
import { ..., exec, ... } from 'child_process'
...
exec($CMD, ...)
- pattern: |
import { ..., execSync, ... } from 'child_process'
...
execSync($CMD, ...)
- pattern: |
import * as $CP from 'child_process'
...
$CP.exec($CMD, ...)
- pattern: |
import * as $CP from 'child_process'
...
$CP.execSync($CMD, ...)
- pattern: |
const $CP = require('child_process')
...
$CP.exec($CMD, ...)
- pattern: |
const $CP = require('child_process')
...
$CP.execSync($CMD, ...)
# A fully literal command has nothing to inject.
- pattern-not: exec('...', ...)
- pattern-not: execSync('...', ...)
- pattern-not: exec(`...`, ...)
- pattern-not: execSync(`...`, ...)

- id: flowfi.javascript-eval
message: >-
`eval` executes its argument as code. Parse the value with `JSON.parse`,
or dispatch to an explicit set of allowed commands.
severity: ERROR
languages: [typescript, javascript]
paths:
include:
- 'backend/src/**'
- 'frontend/src/**'
pattern: eval(...)

- id: flowfi.jwt-none-algorithm
message: >-
A JWT is being verified with the `none` algorithm, which accepts
unsigned tokens and bypasses signature verification entirely.
severity: ERROR
languages: [typescript, javascript]
paths:
include:
- 'backend/src/**'
- 'frontend/src/**'
pattern-regex: '(?i)algorithms?\s*:\s*\[?\s*["'']none["'']'

# -------------------------------------------------------------------
# Cross-site scripting
# -------------------------------------------------------------------
- id: flowfi.react-unescaped-html
message: >-
`dangerouslySetInnerHTML` renders unescaped HTML and is a direct XSS sink.
Render text as children, or sanitise with a vetted library (e.g. DOMPurify)
before passing it in.
severity: ERROR
languages: [typescript]
paths:
include:
- 'frontend/src/**'
pattern: dangerouslySetInnerHTML

# -------------------------------------------------------------------
# Soroban contracts
# -------------------------------------------------------------------
- id: flowfi.contract-unsafe-block
message: >-
`unsafe` is not allowed in contract code. A memory-safety bug in a
contract that holds funds is unrecoverable, so keep the audited surface
free of unchecked pointer operations.
severity: ERROR
languages: [rust]
paths:
include:
- 'contracts/**/src/**'
pattern: unsafe { ... }

- id: flowfi.contract-unwrap-in-production
message: >-
`unwrap()`/`expect()` panics on `None`/`Err` and will abort a contract
invocation. Use `?` with the contract's error type, or return a
contract-appropriate error.
severity: WARNING
languages: [rust]
paths:
include:
- 'contracts/**/src/**'
exclude:
# Tests and property tests are allowed to panic on assertion failure.
- 'contracts/**/src/test.rs'
- 'contracts/**/src/acceptance_tests.rs'
- 'contracts/**/src/property_tests.rs'
- 'contracts/**/tests/**'
pattern-either:
- pattern: $X.unwrap()
- pattern: $X.expect($MSG)
10 changes: 7 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,14 @@ This repository uses GitHub Actions for continuous integration. Workflows are lo
### Available Workflows

- **Security Checks** (`.github/workflows/security.yml`)
- Runs on: push to `main`/`develop`, pull requests, and weekly schedule
- Runs on: push to `main`/`develop`, pull requests, and a weekly schedule
- Performs:
- Dependency vulnerability scanning (`npm audit`)
- CodeQL analysis for JavaScript/TypeScript
- Dependency vulnerability scanning (`npm audit` across workspaces, `cargo audit` for `contracts/`)
- Secret scanning over the full git history (TruffleHog, blocks on verified leaks)
- Static analysis (Semgrep + CodeQL), with results published to the Security tab
- An aggregate `Security Gate` check — require this one in branch protection
- Blocking findings and local reproduction steps are documented in
[SECURITY.md](SECURITY.md#automated-security-scanning)
- View workflow: [Security Checks](.github/workflows/security.yml)

- **CI** (`.github/workflows/ci.yml`)
Expand Down
107 changes: 107 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,113 @@ The frontend application follows security best practices:
- **Secure Dependencies**: Regular dependency updates and vulnerability scanning
- **Wallet Integration**: Secure handling of wallet connections and transactions

### Supply Chain Security

Dependency and secret scanning run automatically in CI — see
[Automated Security Scanning](#automated-security-scanning) for what each check
covers and what blocks a merge. [Dependabot](../.github/dependabot.yml) is
configured for both npm and Cargo and should be preferred over manual upgrades,
so that security bumps follow the same review path as any other change.

## Automated Security Scanning

Every pull request and every push to `main`/`develop` runs the
[Security Checks workflow](.github/workflows/security.yml). A weekly cron job
(Mondays 03:17 UTC) re-runs the same pipeline so that CVEs **published after**
the last dependency bump are caught even when no code has changed.

All scan results are published as SARIF to the repository's
[Security tab](https://github.com/LabsCrypt/flowfi/security/code-scanning).

| Check | Tool | Scope | Blocks a merge? |
| --- | --- | --- | --- |
| Dependency CVEs (Node.js) | `npm audit` | Root + `frontend` + `backend` workspaces | Yes, on high/critical |
| Dependency CVEs (Rust) | `cargo audit` | `contracts/` (Soroban SDK + deps) | Yes, on any advisory |
| Secret scanning | TruffleHog | Full git history, all branches | Yes, on **verified** secrets |
| Static analysis (SAST) | Semgrep + CodeQL | `backend`, `frontend`, `contracts` | Yes, for FlowFi-curated rules |
| Security setup config | `npm run verify-security` | Repository security policy files | Yes, if the policy is missing |

### How blocking works

A single **`Security Gate`** job aggregates the results, and it is the status
check to require in branch protection. Requiring the aggregate rather than the
individual jobs means a contributor sees one failure, and it cannot be bypassed
by re-running a single job.

Each individual check also writes a table to the pull request's
[job summary](https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands#adding-a-job-summary),
so you can triage without digging through raw logs.

### What blocks, and what does not

Blocking is deliberately limited to high-confidence signals, because a security
gate that cries wolf gets ignored:

- **npm audit** blocks on high and critical advisories in **production**
dependencies. Dev-dependency advisories are reported but do not block — a
tooling CVE in a pinned dev tree should not stop a payments hotfix.
- **cargo audit** has no severity model, so any RustSec advisory blocks.
- **TruffleHog** blocks only on secrets it could **verify are live** by
contacting the issuing provider. Unverified candidates (test fixtures,
documentation examples) are not reported and do not block a merge.
- **Semgrep** blocks only on `error`-level findings from the curated
[`.semgrep/flowfi.yml`](.semgrep/flowfi.yml) ruleset. Findings from the
upstream `p/default` ruleset are uploaded to the Security tab for triage but
do not block merges, since an unaudited broad ruleset would otherwise fail
every pull request on day one.

### The curated Semgrep ruleset

`.semgrep/flowfi.yml` holds rules written for this codebase rather than
generically:

- **Credential exposure** — Stellar secret seeds (`S` + 56 base32 characters,
the key that can sign transactions), private key material, and recognisable
provider tokens (GitHub, AWS, Slack, npm, Stripe).
- **Injection** — Prisma `$queryRawUnsafe`/`$executeRawUnsafe` called with an
interpolated string, `child_process` shell execution with a non-literal
command, `eval`, and JWT verification with the `none` algorithm.
- **XSS** — React `dangerouslySetInnerHTML`.
- **Soroban contracts** — `unsafe` blocks, and `unwrap()`/`expect()` in
contract code (warning severity; tracked in the Security tab).

The rules distinguish reviewed patterns from unsafe ones. For example, the raw
SQL calls in `stream.controller.ts` and `withdraw.ts` use a static query with
`$1`/`$2`/`$3` placeholders and are **not** flagged; only a `${...}`
interpolation into an unsafe Prisma call is.

To run the same checks locally before pushing:

```bash
# Static analysis (the curated ruleset only)
semgrep scan --config .semgrep/flowfi.yml --metrics=off backend frontend contracts

# Dependency audits
npm audit --omit=dev --audit-level=high
cargo audit --manifest-path contracts/Cargo.toml

# Secret scanning over the full history
trufflehog git file://. --results=verified --fail
```

If you add or change a rule, re-run it against the existing tree before opening
a pull request. A rule that fires on already-reviewed code is a bug in the rule
and will block everyone until it is fixed.

### If the secret scanner finds something

TruffleHog only fails on credentials it confirmed are live, so a finding is
real. Handle it in this order:

1. **Revoke the credential first.** Purging history does not invalidate a key
that was already pushed.
2. Rotate any related secrets, and check the provider's audit log for use you
did not initiate.
3. Purge the secret from history with `git filter-repo`, then force-push and ask
all collaborators to re-clone.
4. Open a security advisory if the credential was ever reachable from a public
branch.

## Security Best Practices for Users

When using FlowFi, please follow these security guidelines:
Expand Down
Loading
Loading