SECURITY.md
Rama main · réplica de lectura del repositorio original.
# 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/<app>/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/<app>`)
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.