This document records the security decisions, hardening measures, and reporting process for LectureDigest. Read it before changing authentication, input handling, or any code that renders user/AI-generated content — several patches here close real vulnerabilities and are easy to re-open by accident.
Please report security issues privately rather than opening a public issue:
- Open a GitHub Security Advisory (Settings → Security → Report a vulnerability), or
- Email the maintainer listed on the repository profile.
Include reproduction steps and the affected endpoint/file. We aim to acknowledge within a few days. Please do not disclose publicly until a fix is released.
The main branch is the only actively maintained line. Fixes are applied there
and deployed from it.
The frontend is vanilla JS that builds HTML with string concatenation and
innerHTML. This is XSS-prone, so the following rules apply:
- All escape helpers escape quotes.
esc(core.js),_crEsc(chat-rooms.js),_srEsc(study-rooms.js),_engEsc(english.js),_notifEsc(notifications.js), andescHtmlescape& < > " '. Quotes are escaped because values are frequently interpolated inside HTML attributes (e.g.aria-label="...",data-name="..."), not only text nodes. Do not "simplify" these back to atextContent/innerHTMLround-trip — that misses quotes and re-opens attribute-context XSS. - Never interpolate user/AI content into inline event handlers. Patterns
like
onclick="fn('" + name + "')"mix HTML and JS escaping contexts and are unsafe. Use adata-*attribute plusaddEventListenerinstead. See the mention dropdown (chat-rooms.js) and the kick buttons (study-rooms.js) for the correct pattern. - Escape AI output before rendering. Gemini-generated text (titles,
summaries, explanations, quiz content) is treated as untrusted. The chat
renderer escapes before applying markdown; the concept explainer escapes
before converting newlines to
<br>. - Image viewers set
srcvia DOM property, not by interpolating a URL into aninnerHTMLstring (chat-rooms.jscrViewImage).
A CSP header is set in SecurityHeadersMiddleware (backend/main.py). It
whitelists only the origins the app uses: YouTube player, Google Sign-In, the
D3 CDN (jsdelivr), Google Fonts, image/avatar hosts, and the transcript
Cloudflare Worker. It also sets object-src 'none', base-uri 'self',
frame-ancestors 'self', and a restricted connect-src.
Caveat: script-src includes 'unsafe-inline' because the frontend relies
on inline onclick handlers and inline <script> blocks. CSP here is
defense-in-depth (blocks off-origin scripts and data exfiltration), not a
complete inline-injection guard. Removing 'unsafe-inline' would require
refactoring the frontend to external handlers — do that before tightening it.
- Chat
image_urlis server-path-only. The send-message endpoint (routes/chat_rooms.py) rejects anyimage_urlthat is not a server-generated upload path matching/uploads/chat/<token>.<ext>. This prevents a client from storing"><img onerror=...>and turning it into stored XSS for everyone in the room. Keep this validation if you touch the message-send path. - Request body size limits — JSON payloads capped at 10 MB, file uploads at
200 MB (
BodySizeLimitMiddleware). The limit is enforced both from the declaredContent-Lengthand by counting bytes as the body streams in, so a chunked request that omits or understatesContent-Lengthcannot bypass the cap and exhaust memory. The middleware is pure-ASGI (notBaseHTTPMiddleware) precisely so it can see and abort the request stream. - Path traversal — the
/uploads/route and SPA catch-all resolve the absolute path through the_safe_joinhelper, which usesos.path.commonpath(not a naivestartswith) to confirm the target stays inside the allowed directory.startswithalone is vulnerable to prefix-confusion (e.g./app/uploads_evil"starts with"/app/uploads); do not revert to it.
The global exception handler (backend/main.py) never returns raw exception
text to clients. The full traceback is logged server-side with a short random
error_id, and the client receives only a generic "Internal server error"
plus that error_id for correlation. This avoids leaking internal paths, SQL,
or stack details. Keep this behaviour when adding new error handling.
The optional Cloudflare Worker (cloudflare-worker/index.js) only accepts
GET and validates videoId against a strict 11-character YouTube id regex
([A-Za-z0-9_-]{11}) before making any upstream request, so it can't be turned
into an open fetch proxy for arbitrary ids. If you add endpoints to the worker,
keep the method check and input validation, and consider adding rate limiting
and origin restrictions before exposing it publicly.
- Passwords hashed with bcrypt (run off the event loop).
- JWT with expiry; secret comes from
JWT_SECRETenv in production, with a gitignored.jwt_secretfallback for local dev only. - Rate limiting on login (per email), register (per IP), and password reset.
forgot-passwordalways returns success to prevent email enumeration.- Blocklist — blocked emails are rejected at both login and register; admins cannot be blocked or self-deleted.
get_client_ip (routes/client_ip.py) only trusts the X-Forwarded-For header
when TRUST_PROXY is set. Enable TRUST_PROXY=true only when running behind
a trusted reverse proxy (Render/Railway/Fly/nginx); otherwise the spoofable
header is ignored and the socket peer is used. The global rate-limit middleware
uses this helper so users behind a proxy are not lumped into one bucket.
All per-user data (history, notes, bookmarks, folders, English vocab) is scoped
by user_id in the SQL WHERE clause at the DB layer. Room actions
(kick/ban/role/delete/comment) verify the caller's role/ownership before acting.
When adding endpoints that take an id from the client, always scope the query by
the authenticated user or check ownership explicitly.
.env, .jwt_secret, *.sqlite3, *.db, backups/, and uploads/ are
gitignored and excluded from the Docker build context via .dockerignore,
so secrets and local user data never end up committed or baked into an image.
Never commit real keys. Use backend/.env.example as the template.
Security regressions are covered by backend/tests/test_security.py
(image_url validation, path traversal, _safe_join prefix-confusion, body-size
limit, blocked-email enforcement, security headers incl. CSP). Run the full
suite with pytest from backend/ before shipping changes to auth, input
handling, or rendering. CI runs these on every push/PR via
.github/workflows/backend-tests.yml.