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.
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.
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'srequireRole()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 withCORS_ORIGINSset 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.
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_ORIGINSto exactly the frontend origins you run — no wildcard. - Serve every app over HTTPS. Session cookies are not safe over plain HTTP.
- Keep
apps/api/.envout 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.
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.
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.jsoninto an empty directory and runnpm audit --package-lock-onlythere). Auditing a localnode_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.
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.
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.
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:
qs6.15.3 →^6.16.0. Added"overrides": { "qs": "^6.16.0" }toapps/api/package.json. It satisfiesexpress@5.2.1'sqs@^6.14.0andbody-parser'sqs@^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-workspacepackage.jsonin the published tree) — a plain in-placenpm installfrom this monorepo checkout does not apply it, because the siblingdatabounty-v1-parity-rebuildworkspace root shadows it; that root is dev-only tooling, absent from the published repo.nodemailer9.0.6 →^9.1.1. Same major version, no code change needed. Fixes four advisories: GHSA-8m3c-c648-2xjj (disableFileAccess/disableUrlAccess bypass via legacyresolveContent()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).vitest2.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 at3.2.6. Tested before applying: ran the full suite (npm test, which pins--maxWorkers=1 --minWorkers=1because these integration tests share one database) against a fresh disposable Postgres database on both the originalvitest@2.1.9and the bumpedvitest@3.2.7, from a cleangit clone-equivalent standalone copy ofapps/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.
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.jsonand 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.jsoncopied 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,runnerstage) does not removeprisma,mysql2,@prisma/config,deepmerge-ts,express,body-parserorqs, because@prisma/clientpulls 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.