# Security Policy ## Reporting a vulnerability **Please do not open a public issue for a security vulnerability.** A public report tells everyone running this software about the hole at the same moment it tells us. Report privately through **GitHub's private vulnerability reporting**: go to the **Security** tab of this repository → **Report a vulnerability**. This creates a private advisory visible only to you and the maintainers, and needs no email exchange to get started. If you cannot use GitHub advisories, email **support@databounty.io** with `SECURITY` in the subject line. Please do not include the details in an unencrypted attachment you would not want forwarded. Please include, as far as you can: - What the issue is, and which app it affects (`apps/api`, `apps/web`, `apps/admin`, `apps/landing`). - Steps to reproduce, or a proof-of-concept request. - The commit SHA you tested against. - What an attacker gets out of it (read another user's submissions, escalate to `admin`, escape the execution sandbox, and so on). We will acknowledge your report, tell you whether we consider it in scope, and keep you updated as we work on a fix. If we disagree that it is a vulnerability, we will say so and explain why rather than going quiet. Please give us reasonable time to ship a fix before disclosing publicly. We will credit you in the advisory unless you ask us not to. ## Supported versions This project has not yet cut a tagged release. **Only the current `main` branch is supported** — fixes land there, and there are no backports to older commits. If you are running a fork or a pinned commit, you are responsible for pulling fixes forward. ## Scope In scope — anything that lets someone: - Read, modify, or delete data belonging to another user or workspace. - Gain a role they were not granted (`admin`, `member`, `support`), or act past a role check. Note that the API's `requireRole()` is the real security boundary; the admin console's client-side redirect is UX only, and reporting that redirect as bypassable is not a finding by itself. - Escape or abuse the execution sandbox that runs contributed code. - Forge, replay, or steal session cookies, API keys, or MCP OAuth tokens. - Cause the validation pipeline to report a check as passed when it did not run, or was skipped, unsupported, or unconfigured. **We treat dishonest trust state as a security issue, not a cosmetic one** — the whole product rests on those claims being true. - Read files, environment variables, or network destinations the API should not reach. Out of scope: - Missing wallet, payment, escrow, payout, or on-chain functionality. None of it exists in this codebase by design — see [Where we draw the line](README.md#where-we-draw-the-line). Its absence is not a bug. - Findings that require a misconfigured deployment you control, where the fix is your configuration (for example running with a weak `SESSION_SECRET`, or exposing the API with `CORS_ORIGINS` set to `*`). - Optional integrations behaving as documented when unconfigured. Execution sandboxing, LLM review, and notification channels **fail closed and say so** rather than silently passing; that is the intended behavior. - Automated scanner output with no demonstrated impact, missing security headers with no exploit path, or dependency CVEs that are not reachable from this code. - Social engineering, physical attacks, or denial of service by brute traffic volume. ## Running this yourself If you self-host, these are yours to get right, and no upstream fix can cover them: - Set a strong, unique `SESSION_SECRET`. Do not reuse the example value. - Set `CORS_ORIGINS` to exactly the frontend origins you run — no wildcard. - Serve every app over HTTPS. Session cookies are not safe over plain HTTP. - Keep `apps/api/.env` out of version control. It is gitignored here; keep it that way in your fork. - Give the database its own credentials, not a superuser shared with anything else. - Read [DEPLOYMENT.md](DEPLOYMENT.md) for which integrations fail closed and what has to be true for a real deployment. ## Known dependency advisories Production-only `npm audit --omit=dev --package-lock-only` against the per-app lockfiles that CI and the Dockerfiles actually install from, re-run 2026-09-09: | App | Critical | High | Moderate | Total affected packages | | --- | --- | --- | --- | --- | | `apps/api` | 0 | 0 | 0 | 0 | | `apps/web` | 0 | 0 | 0 | 0 | | `apps/admin` | 0 | 0 | 0 | 0 | | `apps/landing` | 0 | 0 | 0 | 0 | **Production is clean across all four apps.** The four `apps/api` high-severity rows that stood here previously (`mysql2`, `deepmerge-ts`, and their carriers `@prisma/config` and `prisma`) were **fixed by tested version overrides on 2026-09-09**, not accepted as exceptions — see below. This table is **production-only by design** (`--omit=dev`), matching what `npm prune --omit=dev` actually ships in the `Dockerfile`'s `runner` stage. `vitest`/`@vitest/mocker` are pure devDependencies — never installed in that image — and remain the one open item; see their subsection below. ### Which lockfile this audit uses, and why it matters `apps//package-lock.json` is the authoritative source. Both `Dockerfile` (`COPY apps/api/package.json apps/api/package-lock.json ./` then `npm ci`) and `ci.yml` (`cache-dependency-path: apps/${{ matrix.app }}/package-lock.json`, `working-directory: apps/`) install from it. In the private development workspace these apps are also npm *workspaces* under a parent `package.json`, and that parent keeps its own lockfile — **npm honours `overrides` only from the workspace root during a workspace install**, so a local `node_modules/` can resolve different (older) versions than the shipped lockfile. That divergence was real and observed: `qs` resolved to the vulnerable 6.15.3 locally while the shipped lockfile already carried the fixed 6.16.0. Two consequences, both load-bearing: - **Audit the per-app lockfile, in isolation from any workspace root** (copy `package.json` + `package-lock.json` into an empty directory and run `npm audit --package-lock-only` there). Auditing a local `node_modules/` reports the developer's tree, not the artefact. - The parent workspace is a private development convenience. It is not exported and never builds a released image. ### Resolved by tested override — 2026-09-09 Declared in `apps/api/package.json` `overrides`: | Package | Was | Now | Advisory closed | | --- | --- | --- | --- | | `mysql2` | 3.15.3 | ^3.24.4 | Auth-plugin downgrade to `mysql_clear_password` leaking plaintext credentials ([GHSA-3f6p-5ww8-9rcr](https://github.com/advisories/GHSA-3f6p-5ww8-9rcr)); unbounded zlib inflate in the compressed protocol handler ([GHSA-rgwj-5xj2-c3m3](https://github.com/advisories/GHSA-rgwj-5xj2-c3m3)) | | `deepmerge-ts` | 7.1.5 | ^8.0.2 | Stack exhaustion when merging recursive object graphs ([GHSA-ggr8-5vv4-36mx](https://github.com/advisories/GHSA-ggr8-5vv4-36mx)) | | `qs` | 6.15.3 | ^6.16.0 | Array-limit bypass via bracket-key comma parsing ([GHSA-x5fp-wj9c-mxmx](https://github.com/advisories/GHSA-x5fp-wj9c-mxmx)); DoS via attacker-controlled `isBuffer` ([GHSA-4mjr-xmp4-gh2g](https://github.com/advisories/GHSA-4mjr-xmp4-gh2g)) — override predates this pass | `@prisma/config` and `prisma` needed no override of their own: both were reported solely as carriers of the two packages above, and both drop off the report once those resolve to patched versions. **This reverses an earlier recorded decision.** The previous revision of this section stated that overriding these transitive versions had been "evaluated and rejected" because it would "mask an advisory in a CLI that never runs in the serving process while risking the config loader." The masking half of that reasoning does not apply — an override to a patched release is an upgrade, not a suppression. The config-loader risk was the real concern, since `@prisma/config@7.9.1` pins `deepmerge-ts` to exactly `7.1.5` and `^8.0.2` is a semver-major jump past that pin. **It was tested rather than assumed**, in an isolated `npm ci` install of the shipped lockfile with the overrides applied: | Prisma CLI path | Where it runs | Result with `deepmerge-ts@8.0.2` | | --- | --- | --- | | `prisma validate` | config load — the exact `@prisma/config` → `deepmerge-ts` path | Passes: *"Loaded Prisma config from prisma.config.ts"*, schema valid | | `prisma generate` | `Dockerfile` `build` stage | Passes: Prisma Client v7.9.1 generated | | `prisma migrate status` | `api-migrate` one-shot container | Passes: connects and reports migration state | | `tsc -p tsconfig.json --noEmit` | whole API source against the overridden tree | Clean, 0 errors | **Residual maintenance risk, stated plainly:** `deepmerge-ts@8` is held past an exact pin by an override. A future Prisma release could depend on 7.x-specific `deepmerge-ts` behaviour, and the override would then force an untested combination. Re-run the four checks above on every Prisma upgrade, and drop the override entirely once Prisma ships a release whose own dependencies are unaffected. ### `vitest`/`@vitest/mocker` — dev-only, the one open item Not part of the production count above on purpose: `vitest` is a direct `devDependency` and `@vitest/mocker` only exists beneath it, so a `--omit=dev` audit — matching what `npm prune --omit=dev` ships in the `runner` stage — never installs either. | Package (installed) | Advisory | Severity | Reachability finding | Decision | | --- | --- | --- | --- | --- | | `vitest` 3.2.7 / `@vitest/mocker` | Path traversal / arbitrary file read via redirect mock ([GHSA-82fw-gwwq-j7x9](https://github.com/advisories/GHSA-82fw-gwwq-j7x9)) | Moderate | **Test-only and pruned from the production image.** Exploiting it requires vitest's own dev/UI server to serve untrusted requests, which happens on a developer machine or in CI, never in a deployed container. The advisory covers `<= 4.1.10`; npm's only fix is `vitest@5.0.0`, a **two-major** jump from the pinned `^3.2.7`. Note for a public repository: a fork pull request does execute test code in CI, but such a PR can already run arbitrary code by definition, so this advisory adds no capability an attacker does not already have — the real control is that fork PRs run without organization secrets (tracked separately in the release checklist). | Scoped exception | **Clearing condition:** a dedicated `vitest` 3 → 5 migration with a full passing test run. Until then this row stays, and it is the only dependency advisory this repository carries. ### Fixed 2026-09-09 — `qs`, `nodemailer`, and vitest's critical advisory Three findings from this section's prior revision are now resolved in `apps/api/package.json` and its lockfile, each verified with a full test run before being applied: - **`qs` 6.15.3 → `^6.16.0`.** Added `"overrides": { "qs": "^6.16.0" }` to `apps/api/package.json`. It satisfies `express@5.2.1`'s `qs@^6.14.0` and `body-parser`'s `qs@^6.15.2`, so this is a **no major-version bump anywhere** fix, exactly as previously recorded as available. Verified the override actually takes effect in a standalone install matching how CI installs this app (`working-directory: apps/api`, no parent-workspace `package.json` in the published tree) — a plain in-place `npm install` from this monorepo checkout does *not* apply it, because the sibling `databounty-v1-parity-rebuild` workspace root shadows it; that root is dev-only tooling, absent from the published repo. - **`nodemailer` 9.0.6 → `^9.1.1`.** Same major version, no code change needed. Fixes four advisories: [GHSA-8m3c-c648-2xjj](https://github.com/advisories/GHSA-8m3c-c648-2xjj) (disableFileAccess/disableUrlAccess bypass via legacy `resolveContent()` signature), [GHSA-wmmp-3585-3rmp](https://github.com/advisories/GHSA-wmmp-3585-3rmp) (IDN/punycode allow-list bypass), [GHSA-2x7j-588g-ccc2](https://github.com/advisories/GHSA-2x7j-588g-ccc2) (quadratic-time address parsing DoS), [GHSA-cc9r-2j5m-2m83](https://github.com/advisories/GHSA-cc9r-2j5m-2m83) (recipient-domain validation bypass via RFC 5322 comment mis-parsing). - **`vitest` 2.1.9 → `^3.2.7`.** A major-version bump, taken deliberately to close a **critical** (CVSS 9.8) advisory — [GHSA-5xrq-8626-4rwp](https://github.com/advisories/GHSA-5xrq-8626-4rwp), arbitrary file read/execution when Vitest's UI server is listening — fixed at `3.2.6`. Tested before applying: ran the full suite (`npm test`, which pins `--maxWorkers=1 --minWorkers=1` because these integration tests share one database) against a fresh disposable Postgres database on both the original `vitest@2.1.9` and the bumped `vitest@3.2.7`, from a clean `git clone`-equivalent standalone copy of `apps/api`. Same 4 pre-existing failures on both versions (`src/pipeline.integration.test.ts`, all dependent on an unconfigured execution/LLM provider producing a different pipeline-stage outcome than the test expects — a known, already-documented gap, not something this bump introduced or fixed) — the version bump changed zero test outcomes. `@vitest/mocker`'s own moderate advisory is *not* fixed by this bump (see the table row above); vitest's own critical advisory is. **Result:** the previously-projected "4 High / 0 Moderate" outcome from removing just the `qs` row did not land exactly as predicted, because two more advisories (the vitest critical one, and `@vitest/mocker`'s moderate one) had been published against this dependency tree since 2026-09-05 and were caught by this same pass. Net: one critical advisory closed, `qs` and `nodemailer` fully closed, one moderate (`@vitest/mocker`) scoped-excepted above. That row now requires **vitest 5**, not 4: the advisory range was subsequently widened to `<= 4.1.10`, so npm's only fix is `vitest@5.0.0`. ### What is *not* proven here Stated plainly, because a dependency claim that overstates its evidence is exactly the kind of dishonest trust state this policy treats as a security issue: - Reachability above was established from `apps/api/package-lock.json` and an installed tree. **Re-verified 2026-09-09 against the generated export candidate's own per-app lockfiles**, each audited in isolation from the development workspace root (`package.json` + `package-lock.json` copied into an empty directory, `npm audit --package-lock-only`): production is 0 across all four apps and the only remaining row is the dev-only vitest one. Re-run this at export time if the lockfiles change again. - The module-load probes cover the static import graph reachable from the entry points this app actually imports. A dependency that reached one of these packages through a *dynamic* import on a code path the probe never triggered would not show up. No such path was found by inspection; that is an inspection result, not a proof. - These packages are still **present on disk** in the production image, now at patched versions rather than absent. `npm prune --omit=dev` (`Dockerfile`, `runner` stage) does not remove `prisma`, `mysql2`, `@prisma/config`, `deepmerge-ts`, `express`, `body-parser` or `qs`, because `@prisma/client` pulls the Prisma CLI in as an optional peer. "Patched" is the claim being made here; "not shipped" is not. - No exploit was attempted against any of these packages. These are reachability findings.