This file is the vulnerability disclosure policy for the repository. It is not a description of
how the API's security works — that lives in docs/SECURITY.md, which
documents the auth flow, password rules, token lifecycle, RBAC model, rate limits, and security
headers. Read that first if your question is "how is this supposed to behave?"; come back here if
the answer is "not like that, and it is exploitable."
This is a starter template rather than a versioned library. Security fixes land on main, and
projects built from the template are expected to pull them forward themselves. Only the current
main branch is supported.
Do not open a public issue for a security vulnerability.
Report it privately through GitHub Security Advisories:
https://github.com/aolus-software/clean-elysia-prisma/security/advisories/new
That creates a private thread visible only to the maintainers. If you cannot use GitHub Advisories, open a regular issue that says only "security report, please provide a private contact" with no technical detail, and a maintainer will follow up.
Please include, as far as you can determine it:
- the affected component (plugin, guard, route, service, repository, config) and
file:lineif you have it, - the conditions required to reach it — authenticated or not, which role or permission, which data,
- a proof-of-concept request or sequence, including the response envelope you got back,
- the impact you believe it has,
- whether it reproduces with
APP_CLUSTER_MODE=false,true, or both.
You should get an acknowledgement within a few days. Please give the maintainers a reasonable window to ship a fix before disclosing publicly.
In scope — anything that ships in this repository:
- authentication and token handling:
AuthPlugin(src/libs/plugins/auth.plugin.ts), JWT issuance via@elysiajs/jwtandJWT_CONFIG, login, register, email verification, password reset, - the RBAC layer:
RoleGuardandPermissionGuardin@guards, and whether a route that should sit below.use(AuthPlugin)actually does, - repository
selectleaks — a query that returns a password hash, raw token, or another field the response schema was supposed to exclude (see.claude/rules/repositories.mdrule 4 and.claude/rules/validation.mdrule 12), - sort/filter allowlist bypasses in list endpoints that let user input reach
orderByorwherekeys, - secret handling: bcrypt password hashing in
@utils,APP_KEY,JWT_SECRET, and anything leaking into logs, error envelopes, or OpenAPI examples, SecurityPlugin(src/libs/plugins/security.plugin.ts): the CORS config,elysia-rate-limitconfiguration, and the helmet CSP directives,BodyLimitPluginand any way to get an oversized or unparsed body past it,- the error contract — an error path that returns a stack trace, a third-party library message, or an
internal identifier to the client (
.claude/rules/errors-and-responses.md), - cluster-mode issues where a security control silently degrades because state is per-worker rather than shared (for example a rate-limit counter that is not backed by Redis),
- dependency vulnerabilities that are reachable through code in this repo.
Out of scope:
- vulnerabilities in a project built from this template that come from that project's own code,
- anything that requires an
.envthe docs tell you not to use — for example leavingALLOWED_HOSTunset (which falls back to*) or running with the example secrets in production, - missing hardening that is documented as the deployer's responsibility (TLS termination, database and Redis network access, secret storage, running the container as a non-root user),
- the ClickHouse analytics path when it is not configured — it is optional infrastructure,
- reports generated by an automated scanner with no demonstrated exploit path.
These are documented rather than treated as vulnerabilities. Review every one of them before deploying anything built on this template.
- Every secret in
@confighas a working default exceptJWT_SECRET.src/libs/config/env.config.tsgivesAPP_KEY,REDIS_PASSWORD, and the mail credentials envalid defaults, so a missing one does not fail startup — the app boots with a publicly known value. Set all of them explicitly and verify at deploy time; do not rely on startup validation to catch an omission. JWT_SECRETis required and has no default.JWT_CONFIG(src/libs/config/jwt.config.ts) signs and verifies every token with it, so it is declaredstr()with no fallback and the process refuses to boot unconfigured. It used to default to"your-secret-key", and a second variableAPP_JWT_SECRETsat next to it onAppConfigreading as if it were the signing key while being read by nothing — so an operator who set only that one shipped forgeable tokens.APP_JWT_SECRETwas removed on 2026-08-21.- CORS defaults to
*.CORSConfigfalls back toorigin: "*"whenALLOWED_HOSTis empty. SetALLOWED_HOSTto your real front-end origins (comma-separated) before exposing the API.credentialsisfalsein the shipped config — if you turn it on, a wildcard origin becomes unsafe rather than merely permissive. - The API reference is env-gated, not secret.
DocsPluginenables/docswheneverNODE_ENV !== "production". Staging deployments publish the full schema. Make sureNODE_ENVis actuallyproductionin production. - Rate limiting is global and generous.
SecurityPluginapplies 100 requests per 60 seconds across all routes, from a single in-process limiter. Tighten it, and add a stricter limit on credential endpoints (login, register, forgot password), before going live. - The rate limiter is per-process. Under
APP_CLUSTER_MODE=trueeach forked worker keeps its own counter, so the effective limit is multiplied by the worker count. Back the limiter with Redis if the limit is load-bearing for you. - Queue workers boot once per worker process.
src/bull/index.tsregisters BullMQ workers on import, and every cluster worker re-runs it. Confirm your jobs are idempotent (.claude/rules/queue.mdrule 9) or gate worker startup on a single process. - The body limit is a fixed 100 KB header check.
BodyLimitPluginreadscontent-length; a request that omits the header is not caught here. Enforce a limit at your reverse proxy as well. - The destructive Make targets do exactly what they say.
make db-push(prisma db push --force-reset),make db-drop(prisma migrate reset --force), andmake fresh(both, then seed) wipe the database in whateverDATABASE_URLyour.envpoints at. So does theprisma migrate devstep in the Huskypre-commithook. Never point a local.envat a shared or production database. - Seed data is not production data.
prisma/seed/creates known accounts, roles, and permissions. Do not runmake db-seedagainst a live environment.