Do not open a public GitHub issue for security vulnerabilities.
Report vulnerabilities by emailing support@consenti.dev with the subject line:
[SECURITY] Consenti — <short description>
Please include:
- Affected package(s) and version(s)
- A description of the vulnerability and its impact
- Steps to reproduce (proof-of-concept code if applicable)
- Any suggested mitigations
You will receive an acknowledgement within 48 hours and a status update within 7 days.
We follow a 90-day responsible disclosure policy. We ask that you refrain from publicly disclosing the issue until we have released a fix or the 90-day window has passed, whichever comes first.
| Version | Supported |
|---|---|
latest (main) |
Yes — security fixes applied immediately |
next (pre-release) |
Best effort |
| older tagged releases | Not actively patched; upgrade recommended |
The following design decisions are security-relevant. Understanding them helps both auditors and operators deploy Consenti safely.
- IP addresses are never stored raw. Every IP is SHA-256 hashed before persistence:
createHash('sha256').update(ip).digest('hex'). The original IP cannot be recovered. - Visitor IDs use
crypto.randomUUID()— no sequential integers that could be enumerated.
- Passwords: scrypt via
node:crypto— no external bcrypt dependency. - JWTs: signed with
createHmac('sha256', secret)vianode:crypto— nojsonwebtokenpackage. - All admin routes require a valid JWT via
auth.middleware.ts. No admin endpoint is reachable without authentication. - RBAC: fine-grained permissions enforced at the service layer.
audit_logsis append-only / never deleted by Consenti — the storage adapter never issuesUPDATEorDELETEagainst this table, under any configuration. There is no retention-purge option for this table (unlikecompliance.dataRetention.purgeAfterDays, which purges expired consent records and is unrelated).- This provides append-only evidence for GDPR accountability obligations. It is not cryptographically tamper-evident (no chaining/hashing between entries) — see "Known Limitations."
- Operators who need to reduce audit-log retention for their own storage or operational reasons must do so manually, directly against their own database. This is intentionally outside Consenti's supported paths — there is no config flag or admin action that deletes audit log rows.
- Stack traces are suppressed in
NODE_ENV=productionresponses. Only a structured JSON error with a code and message is returned.
apps/uiandapps/apihave zero external runtime dependencies. No transitive supply-chain risk from npm packages.
When deploying Consenti in production:
- Set
NODE_ENV=productionto suppress stack traces - Configure a strong
auth.adminPassword(≥ 16 chars, env var — never hardcoded; bootstrap enforces ≥ 12 chars minimum and warns below 16) - Set a random
jwt.secretof at least 32 bytes (env var) - Set
cors.originsto an explicit allowlist — never use'*'in production - Restrict filesystem access to the SQLite DB file (mode
600, owned by the app user) - Put Consenti behind a reverse proxy (nginx / Caddy) with TLS termination
- Add
Content-Security-Policy,X-Frame-Options, andStrict-Transport-Securityheaders at the proxy layer - Enable rate limiting (built-in) and configure
rateLimit.windowMs/rateLimit.maxfor your traffic - Rotate the JWT secret if it is ever exposed; all active sessions will be invalidated
The following are known trade-offs, not vulnerabilities, but operators should be aware of them:
| Limitation | Notes |
|---|---|
Standalone @consenti/ui HMAC signing is spoofable |
With no @consenti/api backend, core.cookieSigningKey has nowhere to live but the shipped browser bundle — anyone can read it and re-sign a forged cookie. It catches accidental tampering, not a motivated attacker. Server-side verification already exists via @consenti/api's compliance.dataSigningHash (the signing key never reaches the browser) — use it whenever you need consent records that hold up as evidence, not just client-side tamper-evidence. |
Consent cookies lack HttpOnly |
The consent widget intentionally reads consenti_* cookies via JavaScript to display consent state. HttpOnly cannot be applied globally. The visitorId embedded in the cookie name is a persistent identifier that XSS payloads can read — mitigate by deploying a strong CSP that prevents script injection. |
| Plugin hooks run in-process with full Node.js access | Only install plugins from trusted sources. No sandbox exists today. |
| SQLite DB file permissions are operator responsibility | The file must not be web-accessible. Configure your web server to block direct file access. |
| No built-in CSP headers | Add Content-Security-Policy at your reverse proxy or web server layer. |
| Swagger UI loaded from unpkg CDN | The API documentation page (/api/docs) loads swagger-ui-dist@5.17.14 from https://unpkg.com. If operating in a high-security environment, pin to a specific version and add integrity="sha384-…" SRI hashes (compute with `openssl dgst -sha384 -binary file |
Status: not yet conducted. Everything in "Security Architecture" and "Known Limitations" above is Consenti's own self-assessment — accurate to the best of the maintainers' knowledge, but not independently verified. This section exists so that status is stated plainly rather than left implicit, and so a future review has a scoped starting point instead of "audit everything."
- Authentication — password hashing (
scryptvianode:crypto,apps/api/src/utils/crypto.ts), admin JWT issuance/verification (createHmac('sha256', secret), nojsonwebtokendependency), session/token lifecycle (auth.middleware.ts), OAuth/OIDC/SAML integration points. - Authorisation — RBAC permission checks at the service layer (per-route
authenticate()+authError()calls against a named permission, e.g.settings:updateintenants.routes.ts), and the split betweenroutes/admin/*(JWT-gated, permission-checked) androutes/public/*(consent,profile,notice,tcf-status,gpp-status— visitor-facing, unauthenticated by design). - Storage — all seven adapters (
postgresql,mysql,mongodb,json, and the threesqlitevariants) for injection risk, especially anywhere a query is composed outside the parameterised-query helpers; the audit-log append-only guarantee (see "Audit Log Integrity" above) — confirm no code path canUPDATE/DELETEaudit_logsunder any configuration. - Public routes — rate limiting coverage and bypass potential, CORS configuration, input
validation on every
routes/public/*endpoint (these are reachable with no authentication by design, so they're the widest attack surface). - Cryptography — the
hashIp()/hashUserAgent()masking+salting scheme inutils/crypto.ts, cookie-signing (compliance.dataSigningHashserver-side vs. the spoofable client-only fallback documented in "Known Limitations"), and the parental-consent token scheme (utils/parental-consent-token.ts). - Full context for a reviewer: this file,
THREATMODEL.md(assets, trust boundaries, STRIDE analysis, mitigations already in place, known gaps), andAGENTS.md(repo conventions).
A findings summary — severity, affected component, remediation status — gets added to this section once a review completes. Whether the full report is published depends on whether it contains exploit-level detail; the summary here will not.
Not something this document can commit to — engaging a specific firm or independent researcher is
a maintainer decision (budget, timing, availability) outside what's fixable in a docs pass. If
you're a security researcher interested in reviewing Consenti, or can point to one, reach out via
the vulnerability-reporting contact above with subject line [SECURITY REVIEW].
There is no formal bug bounty program at this time. Responsible disclosure is appreciated and all valid reports will be credited in the release changelog (with the reporter's permission).