Status: alpha. This document describes the security model and the protections in place. Review responsibly — see "Reporting" below.
| Threat | Mitigation |
|---|---|
| XSS via uploaded card HTML | Card HTML is sanitised with DOMPurify at ingest (server/src/services/cardRenderer.ts); the web client re-scopes CSS and escapes <. A strict CSP (script-src 'self') is the browser backstop. |
| XSS via model CSS | CSS is sanitised server-side (sanitizeCss strips @import/@charset/external url() and all <); the web scopeCss re-applies the same controls. |
Path traversal via slug |
Admin/add-on slugs are always run through slugify (sanitizeUserSlug); deckApkgPath and safeJoin enforce path containment. Verified by test. |
| Path traversal via media | mapped_name is server-generated (hash); resolveMedia validates ^[\w.-]+$ and resolves within the deck's media dir. ..%2F… → 404. |
| Brute-force site/admin/user password | Rate limits: /site/unlock + /admin/login 5/min/IP, /auth/login 10/min/IP, /auth/register 5/min/IP. app.set('trust proxy', TRUSTED_PROXY_HOPS) ensures correct client IP behind Coolify/Traefik. |
| Account enumeration | Generic INVALID_CREDENTIALS; uniform-timing admin login (bcrypt runs before the username compare). Register returns USERNAME_TAKEN but is rate-limited. |
| API key leakage | Keys are argon2id-hashed; lookup via indexed key_prefix; shown once; revoked keys and wrong keys return identical 401 timing/shape. The X-API-Key header is redacted from logs. |
| Session theft | Cookies are httpOnly; secure (prod); sameSite=lax. JWTs are HS256 with a ≥32-char secret; the server refuses to start in production with default secrets. |
| CSRF | State changes require POST/PUT/DELETE + JSON content-type (not preflight-free cross-origin) combined with sameSite=lax. /api/upload uses a header API key (not cookie) so is not CSRF-exposed. |
| Large-upload DoS | MAX_UPLOAD_SIZE_MB enforced at multer (413); upload endpoints are rate-limited (20/hour/IP). |
| Information disclosure | Production errors return internal error (no stack); helmet sets COEP/CORP/COOP; web sets X-Frame-Options: DENY, Referrer-Policy, Permissions-Policy. |
- JWT server-side revocation list. Logout clears the cookie; a stolen token
is valid until
JWT_EXPIRY. Mitigation: shortJWT_EXPIRY(default 7d) and rotateJWT_SECRETto invalidate all sessions if a breach is suspected. - Legacy image-occlusion SVG format. Native Anki 23.10+ IO is supported;
the legacy "Image Occlusion Enhanced" add-on format is descoped (see
NOTES.md). - Single admin. One admin from env vars, by design.
- Set all four secrets to strong random values (the server enforces this).
- Set
TRUSTED_PROXY_HOPSto the number of proxies in front of the server. - Schedule nightly
server/scripts/backup.sh. - Put Coolify/Traefik TLS in front (HTTPS only).
- Restrict network access to Postgres (it should not be publicly reachable).
This is a personal/hobby project without a formal bug-bounty programme. If you
find a security issue, please open a private GitHub Security Advisory
(Security tab → Report a vulnerability) or email the repo owner. Please do
not open a public issue for security problems.