Looking for how to secure an app built with this module — the CSP, the auth proxy, SSR tokens, safe redirects, and a production checklist? That's the security guide. This file covers reporting vulnerabilities in the module.
Only the latest release of nuxt-convex-module receives security fixes.
| Version | Supported |
|---|---|
| latest | ✓ |
Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.
Use GitHub's private vulnerability reporting instead:
(Also reachable from the repository's Security tab → Report a vulnerability.)
This opens a private draft advisory visible only to you and the maintainers. It keeps the whole process — report, discussion, fix, credit, and publication — in one place, and no report can be lost in a mailbox.
If you cannot use GitHub for any reason, email razum@qruto.to with the subject line
[nuxt-convex-module] Security Vulnerability.
- A description of the vulnerability and its potential impact
- The affected version, and which part of the module is involved (module setup, a runtime composable, a server route, the auth proxy, the generated CSP, …)
- Steps to reproduce — proof-of-concept code or a minimal reproduction repository if possible
- Any suggested mitigations
- Acknowledgement within 48 hours in the advisory thread (or by email if you reported that way).
- We confirm the report, assess severity, and agree a fix timeline with you — targeting 7 days for critical issues.
- The fix is developed in the advisory's private fork, released, and the advisory is published
through the GitHub Advisory Database, which propagates it to
npm audit, Dependabot, and other consumers of the module. - A CVE is requested through GitHub where the issue warrants one, and you are credited in the published advisory unless you ask to stay anonymous.
We follow coordinated disclosure: we ask that you give us reasonable time to patch and publish before disclosing publicly.
In scope is code shipped by this package — module setup, runtime composables and components, Nitro server handlers, the Better Auth proxy routes, and the Convex-aware CSP this module generates.
Vulnerabilities in the upstream projects the module integrates with — Convex, Better Auth, or Polar — belong in their security process, not here. Report them to that project directly. If you are unsure which side a finding lands on, report it here and we will route it.
Reporting is the last line, not the first. These are the gates a change passes before it can reach a release.
fallow security runs as its own step in the static job and in
the pre-push hook. It has to be its own step: fallow keeps security findings out of both its
default run and its audit gate — "this command is the only surface for security findings" —
so neither of the checks already in that job would ever report one. Wiring the rules into
.fallowrc.jsonc without a dedicated step would look configured and scan nothing.
It covers 44 candidate categories: injection sinks (SQL, NoSQL, command, code, template), SSRF,
path traversal, prototype pollution, unsafe deserialization, XXE, SSTI, open redirect,
permissive CORS, weak crypto, disabled TLS validation, postMessage wildcard origins, and the
rest. The scan is whole-repo rather than changed-files-only — it costs about 0.14s, the repo
sits at zero candidates, and a candidate in a file your branch never touched should still block.
Candidates are unverified by design. Fallow matches syntactic sink shapes against a CWE
catalogue; it does not prove anything reaches them. A red build means go and look, not a
vulnerability shipped. A candidate confirmed harmless gets // fallow-ignore-file security-sink
with the reason it is harmless.
Two categories stay off on purpose. hardcoded-secret and secret-to-network fire only when
named in security.categories.include — but that setting is a whitelist, so naming them
restricts the run to them and drops the other 44. Measured against a fixture holding an exec()
sink, an innerHTML sink and a Stripe-shaped key: with no config fallow found both sinks and
missed the key; with include: ["hardcoded-secret"] it found the key and neither sink. Keeping
both halves would mean hand-listing all 46 ids, after which every category fallow adds in a
future release is silently absent — no error, no warning, coverage quietly stops growing.
Credentials are GitHub secret scanning's job instead (see below).
fallow security is the fast deterministic pass. deepsec is
the thorough one: a free regex scan, then an AI stage that reads each candidate in its actual
context, then a revalidation stage that cuts the false-positive rate. Run it before a release,
after touching the Better Auth proxy or any server handler, and whenever a fallow candidate needs
a judgement call rather than a glance:
cd .deepsec
pnpm deepsec scan # free, regex only
pnpm deepsec process --concurrency 5 # the AI stage
pnpm deepsec revalidate --concurrency 5 # cuts the false-positive rate
pnpm deepsec export --format md-dir --out ./findingsThe workspace is deliberately not in git, and must stay that way: data/*/INFO.md maps this
repo's security-sensitive surface, and exported findings describe vulnerabilities that are not
fixed yet. Neither belongs in a public repository. npx deepsec init recreates it; .deepsec/README.md
covers setup and model credentials once it exists.
| Gate | What it stops |
|---|---|
minimumReleaseAge: 1440 (pnpm) |
A package published in the last 24h cannot be installed at all — the window a malicious publish relies on |
Dependabot cooldown: 2 days |
Kept one day wider than the pnpm gate so it never proposes a version pnpm will refuse. Security updates are exempt, so CVE fixes ship immediately |
dependency-review (PRs) |
Known CVEs and malware in the dependency delta |
verifyDepsBeforeRun: error |
A lockfile that no longer matches the manifests |
pack job |
Builds the real tarball, lints its shape with publint + attw, and installs it into a fixture with plain npm |
Dependabot owns every dependency PR — npm versions and GitHub Actions digests alike. No third-party app holds write access to this repository.
- Every third-party action is pinned to a full commit SHA, never a tag — and GitHub enforces it
(
sha_pinning_required), so a floating tag is refused before zizmor ever sees it. persist-credentials: falseon every checkout. The one job that pushes carries the token in a single step's environment, so it never sits in.git/configwhile dependencies install.- zizmor statically analyses the workflows themselves — template injection, unpinned actions, impostor commits, credential persistence. Accepted findings carry their reasoning.
- The release is three jobs so the credentials never meet the code: the job that tags holds no npm
credential and cannot start until a maintainer approves the run; the job that builds holds
nothing; the job that publishes checks out nothing, installs nothing, runs behind
step-security/harden-runnerinblockmode with an allowlist of GitHub, npm and Sigstore, and works in a fixed order — attest the tarball, create the GitHub Release, then stage on npm — so nothing reaches npm without its proof.Release Prepareis split the same way: the job that runs changelogen has a read-only token, and the job that commits and opens the pull request installs nothing. - Every release carries its own proof: the tarball and its SBOM are attested with
actions/attest-build-provenance(gh attestation verify nuxt-convex-module-<version>.tgz --owner qruto), the attestation bundle and the CycloneDX SBOM of the package's production dependencies are assets of the immutable GitHub Release, and npm's provenance covers the same tarball. - No stored credentials at all. Publishing uses npm Trusted Publishing over OIDC bound to
a
main-only environment, and coverage uploads use Codecov's OIDC — so no long-lived token exists anywhere in this repository, and a workflow edited on a branch cannot reach either service.id-token: writeis granted only to jobs that run no pull-request-authored code and install nothing. Releases are staged: nothing becomes installable until a maintainer approves it with 2FA, after npm's malware scan — provenance says where a package came from, not what is in it. - The release refuses a commit whose
cirun is not a completed success, and waits for one that is still running rather than re-running its jobs.
fallow deliberately leaves credentials to GitHub, so these settings are what catch them:
| Setting | State | Why |
|---|---|---|
| Private vulnerability reporting | on | The reporting path at the top of this file |
| Dependabot security updates | on | Advisories against the resolved lockfile |
| Dependabot malware alerts | on | The one thing the dependency graph does not cover: a malicious publish of an already-pinned transitive |
| Secret scanning | on | Detects a committed credential |
| Push protection | on | Blocks the commit outright instead of reporting it after the fact |
| Code scanning (CodeQL) | on, extended |
The default suite plus 16 JS/TS and 5 actions/* queries |
| Non-provider patterns | off, deliberately | Generic keys and connection strings with no vendor prefix — a documented false-positive class |
| Validity checks | off | Would tell us whether a detected key is still live, but needs Team or Enterprise; this org is on the free plan |
Everything above protects this repository. What the module does for an application built on it — the Convex-aware CSP, the hardened auth proxy, auth tokens in SSR payloads, safe post-sign-in redirects, cross-domain one-time tokens — is documented in the security guide.
The attacker-reachable surface — the auth proxy, the one-time-token exchange, the SSR token
prefetch, the auth middleware, the redirect guard, the CSP, and the DevTools panel — was
reviewed by hand before 1.0.0 (2026-09-13), one question per file. What was found, and what
was decided:
- Proxy path. Nitro routes the raw request path and the handler forwarded the URL-normalised
one, so
/api/auth/../../xmatched/api/auth/**and was sent to the site origin as/x— any HTTP action on the deployment, through the app's origin, with its cookies. Next.js normalises paths before routing, which is why upstream's handler never sees one. Fixed: the proxy refuses a request whose normalised path differs from the routed one (400). - Proxy forwarded host.
x-forwarded-host/x-better-auth-forwarded-hostcarry the incomingHost, exactly as upstream'sconvexBetterAuthNextJsdoes. A spoofedHostreaches Better Auth on the Convex side as the request origin. Accepted, with the upstream mitigation: setbaseURLandtrustedOriginsin the Convex-side Better Auth config, as its docs require. The response relay is verbatim (Set-Cookie,Location); the origin it relays from is pinned by config, so nothing foreign is relayed. - Cross-domain tokens.
crossDomainCallbackRouteis off by default, matching upstream: a fresh token completes sign-in on whatever page receives it. Accepted as upstream's design; innuxt devthe exchange now warns once when no callback route is set, naming the option. - SSR token prefetch.
Cache-Control: private, no-storeis set only when a token exists. Without one the payload carries no per-user data — Convex enforces auth inside the function — so a signed-out render is cacheable. Accepted. A customtokenpassed touseAsyncQueryis the caller's to protect. authmiddleware. Undernuxt generatethe guard runs with a request event and no cookie, so a protected page prerenders as a redirect tologinPath— nothing guarded is baked in. Without a request event it used to let the page render. Fixed: it fails closed.- Redirect guard.
resolveAuthRedirectre-resolves every candidate against the app origin and refuses anything that lands elsewhere; the backslash, scheme-relative, dot-segment, percent-encoded and control-character spellings are all covered by its tests. No change. - CSP. Origins are derived through
new URL()'sorigin/host, so a malformedconvex.urlyields no source rather than a mangled directive. No change. - DevTools. The panel and its RPC run only under
nuxt dev, on Vite's middleware — reachable exactly as far as Nuxt DevTools itself.getInfoexposes the deployment URLs and the project's root path, information Nuxt DevTools already shows. Accepted.