Skip to content

Security: thies2005/AnkiShare

Security

docs/SECURITY.md

Security

Status: alpha. This document describes the security model and the protections in place. Review responsibly — see "Reporting" below.

Threat model & mitigations

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.

What is intentionally NOT in scope (v1)

  • JWT server-side revocation list. Logout clears the cookie; a stolen token is valid until JWT_EXPIRY. Mitigation: short JWT_EXPIRY (default 7d) and rotate JWT_SECRET to 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.

Operational hardening checklist (for the operator)

  1. Set all four secrets to strong random values (the server enforces this).
  2. Set TRUSTED_PROXY_HOPS to the number of proxies in front of the server.
  3. Schedule nightly server/scripts/backup.sh.
  4. Put Coolify/Traefik TLS in front (HTTPS only).
  5. Restrict network access to Postgres (it should not be publicly reachable).

Reporting

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.

There aren't any published security advisories