Skip to content

Security: Databounty-io/databounty

Security

SECURITY.md

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. 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 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); unbounded zlib inflate in the compressed protocol handler (GHSA-rgwj-5xj2-c3m3)
deepmerge-ts 7.1.5 ^8.0.2 Stack exhaustion when merging recursive object graphs (GHSA-ggr8-5vv4-36mx)
qs 6.15.3 ^6.16.0 Array-limit bypass via bracket-key comma parsing (GHSA-x5fp-wj9c-mxmx); DoS via attacker-controlled isBuffer (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) 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 (disableFileAccess/disableUrlAccess bypass via legacy resolveContent() signature), GHSA-wmmp-3585-3rmp (IDN/punycode allow-list bypass), GHSA-2x7j-588g-ccc2 (quadratic-time address parsing DoS), 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, 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.

There aren't any published security advisories