Please do not open a public GitHub issue for security vulnerabilities.
Report privately from this repository: https://github.com/fanilabs/backend/security/advisories/new — or click Report a vulnerability under this repository's Security tab. GitHub's private vulnerability reporting is enabled for this repo, so this form is a direct, self-contained channel to the maintainers: no email address to guess, no other repository to locate first. The report, the discussion, and fix coordination all stay private until a patch ships.
Please include a description, reproduction steps, and an impact assessment. We aim to acknowledge reports within 5 business days.
The FaniLab organization contact in the smart contract repository's
SECURITY.md remains valid as supplementary context, but the advisory form
above is sufficient on its own and is the preferred route.
CODE_OF_CONDUCT.md's Enforcement section points here for conduct reports as
well — use the same Report a vulnerability form; it reaches the same
maintainers and stays private.
This backend never holds private keys for senders, recipients, drivers, or fleet owners. Every contract call requiring one of those parties' require_auth() is built as an unsigned XDR transaction and returned to the client for wallet signing (see AUTHENTICATION.md). This eliminates an entire class of risk (a compromised backend cannot move user funds) by construction, not by policy — there is no code path that constructs a signature for a user-owned action.
- Helmet (
@fastify/helmet) with a strict default CSP (default-src 'self',object-src 'none'); relaxed only for the Swagger UI route, never globally. - CORS: explicit allow-listed origins (
CORS_ORIGIN), credentials only for those origins — never a wildcard with credentials enabled. Enforced at boot, not just by convention:src/shared/config/env.tsparsesCORS_ORIGINinto an array, rejects*outright, and rejects any entry that isn't a well-formedscheme://host[:port]origin (no path, no trailing slash, no empty segments from a stray comma) — a misconfigured value fails startup with a clear error instead of silently never matching at request time. - Rate limiting: Redis-backed (
@fastify/rate-limit), so limits hold across horizontally scaled API instances rather than resetting per-process. - Input validation: every route's request body/params/query validated by a Zod schema (
fastify-type-provider-zod) before handler code runs — rejected requests never reach application logic. - SQL injection: Prisma's parameterized queries throughout; no raw string-interpolated SQL. The one
$queryRawusage (src/shared/http/routes/health.ts) is a static, parameter-freeSELECT 1.
- All secrets (
JWT_ACCESS_SECRET,JWT_REFRESH_SECRET,DATABASE_URL,CONTRACT_DEPLOYER_KEY-equivalents if ever needed) are read from environment variables, validated at boot (src/shared/config/env.ts) — the process refuses to start with a missing or too-short secret rather than falling back to an insecure default. .envis git-ignored;.env.exampledocuments every variable without real values.- Logs redact
authorization/cookieheaders and any field namedpassword,passwordHash,token,accessToken,refreshToken— both as a bare top-level key and one level nested (src/shared/logger/index.ts'sredactConfig) — this is enforced at the logger level, not left to call-site discipline. The bare-key paths were added after finding that Pino's'*.token'-style wildcard only matches a key nested one level under the merge object, not a top-level one — the exact shapecreateLoggerMailerlogs ({ to, token }) went unredacted before this fix; seesrc/shared/logger/redact.spec.tsfor the regression test. - The
logger-backedMailer/NotificationSenderdev-defaults are refused at boot whenNODE_ENV=production(MAIL_PROVIDER/NOTIFICATION_PROVIDERinsrc/shared/config/env.ts, enforced inselect-mailer.ts/select-notification-sender.ts) — a production deployment with no real provider configured fails loudly instead of silently sending no mail.
- Local accounts: bcrypt-hashed passwords, JWT access/refresh tokens with rotation-on-use, RBAC (
CUSTOMER/COURIER/FLEET_MANAGER/ADMIN). Full detail:AUTHENTICATION.md. - On-chain authorization is enforced by the contracts themselves (
require_auth()) — this backend does not and cannot bypass that; it only ever proposes transactions for the rightful signer to authorize.
Privileged/sensitive actions (admin dispute resolutions, KYC status changes, role changes) are recorded in the audit_logs table with actor, action, entity, and timestamp — a structural requirement of the admin module design (ARCHITECTURE.md §4), not an afterthought bolted on later.
Phase 6 (first dedicated pass): a full-codebase review covering auth/authorization, IDOR, path traversal, injection, cryptographic token handling, mass assignment, SSRF, and sensitive-data exposure. One real, fixed finding: disputes evidence upload/download had no ownership check at all — any authenticated user could upload arbitrary content to any open dispute under any address's name, and download any dispute's evidence file given only its id (itself discoverable via the public dispute-read endpoint). Fixed by requiring the caller to own the wallet they're uploading as, and restricting download to ADMIN/the uploader/the dispute's raiser — see API_REFERENCE.md's disputes section and src/modules/disputes/application/{upload,download}-evidence.ts. Everything else reviewed (JWT handling, wallet-link challenge binding, password-reset token design, evidence-storage path-traversal guard, CORS/Helmet config, error-message/log redaction) held up without changes needed.
From PHASE_1_DOMAIN_ANALYSIS.md §3: escrow_contract.freeze_funds has no require_auth check at all on-chain — callable by anyone. The backend does not rely on this function being access-controlled and does not expose an unauthenticated route that triggers it; only the dispute_resolution_contract's own authorized flow calls it, on-chain, outside this backend's control.
Dependabot (.github/dependabot.yml) tracks npm, Docker base images, and GitHub Actions weekly; major version bumps require manual review rather than auto-merge.
Dependabot only proposes upgrades — it does not fail a build for a known-vulnerable dependency that hasn't been upgraded yet. To close that gap, the audit job in .github/workflows/ci.yml runs pnpm audit --audit-level=high on every PR and on main; a high or critical advisory anywhere in the dependency tree (direct or transitive) fails CI. Run it locally with pnpm audit.
Accepted exceptions: none currently at or above the high CI threshold. If an advisory has no available fix and must be temporarily tolerated, it must be listed here with the advisory id, the affected package, a rationale, and an owner — the audit threshold is never lowered globally to work around a single unfixable advisory.
Known residual advisories below the high threshold (do not fail CI, recorded here for visibility):
GHSA-w5hq-g745-h8pq—uuid<11.1.1, missing buffer bounds check in v3/v5/v6 when abufargument is supplied. Reached only viaautocannon > hyperid > uuid;autocannonis a dev-only load-testing tool (pnpm load-test), never bundled or run in production, and this project never passes abufargument. No override is applied becausehyperidhas not published a release depending on a patcheduuid.
The pnpm.overrides block in package.json pins forward-patched versions of tar, handlebars, vite, esbuild, nanoid, js-yaml, and fast-uri — all pulled in transitively through build/lint/test tooling (bcrypt's native-build toolchain, eslint-plugin-boundaries, the Vitest/Vite stack) — to clear the critical/high advisories those chains carried.
Coordinated disclosure: we ask reporters to give us a reasonable window to ship a fix before public disclosure. Credited in release notes unless anonymity is requested.