Last scan: 2026-04-20 (original Rust/Axum stack). Updated paths for Go/chi migration.
This document captures the findings of a full-codebase security review covering authentication, authorization, input validation, XSS/CSRF, secrets handling, transport security, rate limiting, business logic, and dependencies. Issues are grouped by severity with concrete fixes.
Lextures is a Go (chi) backend + React/TypeScript frontend LMS. The codebase demonstrates generally good security practices — parameterized SQL (pgx), Argon2id password hashing, RBAC-based permission checks, and proper JWT expiry. However, several High severity issues must be remediated before production: JWT storage in localStorage, no CSRF protection, no rate limiting on auth, and missing security headers.
Overall posture: MEDIUM. Do not ship to production until all P0/P1 items below are resolved.
.envis gitignored; onlyserver/.env.example(placeholders) is tracked.- Argon2id password hashing (
server/internal/service/authservice/credentials_password.go) - Parameterized SQL via pgx everywhere (no string-interpolated user input in queries)
- JWT implementation uses
golang-jwt/jwt(no OpenSSL surface) - Reset tokens are SHA-256 hashed before DB storage, one-time use, 1h expiry
- Comprehensive RBAC with explicit permission checks on routes
- TLS via Go's standard
crypto/tlswith modern defaults
- JWT stored in
localStorage(XSS-exposable) - No CSRF protection; CORS wide open (
Any) - No rate limiting on
/auth/login,/auth/signup,/auth/reset-password - No security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
- Client-side-only quiz lockdown enforcement
- Potential IDOR on accommodations and quiz results endpoints
dangerouslySetInnerHTMLwithout DOMPurify on KaTeX output
- File: clients/web/src/lib/auth.ts:14, clients/web/src/lib/auth.ts:24
- Issue: Access token written to
localStorage; readable by any script on the page. - Risk: A single XSS anywhere in the app leaks the JWT, enabling full session hijack.
- Fix: Issue the token as an
HttpOnly; Secure; SameSite=Strict; Path=/apicookie from the backend. Remove alllocalStoragetoken usage. UpdateauthorizedFetchto rely on cookie transmission (credentials: 'include').
- Files: server/internal/httpserver/server.go, all mutating routes in
server/internal/httpserver/ - Issue: No CSRF token validation and CORS is configured with
allow_origin(Any)+allow_headers(Any). - Risk: If JWT is moved to a cookie (H1), attacker-origin pages can forge state-changing requests (enrollments, grade edits, quiz submits).
- Fix:
- Restrict CORS to an explicit allowlist of trusted origins.
- Implement double-submit CSRF token: issue a CSRF cookie on login, require matching
X-CSRF-Tokenheader on all non-GET routes. - Use
SameSite=Stricton the auth cookie as defense-in-depth.
- File: server/internal/httpserver/server.go
- Issue: No CSP, HSTS, X-Frame-Options, X-Content-Type-Options, or Referrer-Policy set.
- Risk: Clickjacking, MIME sniffing, unrestricted script sources.
- Fix: Add a chi middleware that sets response headers:
Strict-Transport-Security: max-age=31536000; includeSubDomains X-Frame-Options: DENY X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;
- File: server/internal/httpserver/auth.go
- Issue:
/auth/login,/auth/signup,/auth/request-password-reset,/auth/reset-passwordaccept unlimited attempts. - Risk: Credential stuffing, brute force, account enumeration, password-reset spam.
- Fix: Add a rate limiting chi middleware. Recommended buckets:
- Login: 5 failures / 15 min per IP, 10 / hr per email
- Signup: 3 / hr per IP
- Reset request: 3 / hr per email
- File: server/internal/repos/coursefiles/paths.go
- Issue:
DiskCourseDirSegmentreplaces unsafe chars but does not reject.., multiple dots, or absolute segments pre-sanitization. File storage root relies on this being safe. - Risk: Path traversal to read/write outside
course_files_root. - Fix: After joining, use
filepath.EvalSymlinks/filepath.Absand assert the result is still prefixed bycourse_files_root. Reject any segment containing... Use UUIDs (not user-supplied names) for on-disk filenames.
- File: server/internal/httpserver/accommodations.go
- Issue: Handler accepts
{user_id}path param; only verifies caller hasMANAGE_ACCOMMODATIONS_PERM. No check that the target belongs to a course the caller manages. - Risk: Any user with the permission (e.g., a single-course TA) can read/edit accommodations for every user system-wide.
- Fix: Scope the permission check to courses: require
course:<code>:accommodations:manageand verify the target user is enrolled in one of the caller's managed courses.
- File: server/internal/httpserver/module_quiz.go
- Issue:
student_user_idis trusted oncecan_editis true; no verification that the student is enrolled in the course being viewed. - Risk: An instructor in course A can enumerate/download attempt data for a student only enrolled in course B.
- Fix: After resolving
target_user, call anenrollment.UserIsEnrolled(ctx, courseID, targetUser)check and returnForbiddenif false.
- File: server/internal/httpserver/gradebook_grid.go (
GradebookGradesPutHandler) - Issue: Permission gate is
course_item_create_permission— the same permission used for creating assignments. A role that is allowed to author items should not implicitly be allowed to overwrite grades. - Risk: Overly broad permission can be abused to change grades.
- Fix: Introduce a distinct
course:<code>:gradebook:writepermission and gate grade mutations on it. Log all grade mutations to an audit table (course_id, actor_id, student_id, item_id, old→new, timestamp).
- File: server/internal/service/quizlockdown/lockdown.go, clients/web/src/components/quiz/QuizStudentTakePanel.tsx
- Issue:
one_at_a_timeandkioskmodes rely on the client hiding questions and disabling navigation. A student with DevTools can submit arbitrary combinations. - Risk: Bypasses intended exam integrity.
- Fix: Track server-side attempt state per question (question index, first-seen timestamp, answered flag). Reject submissions that violate the mode's invariants (e.g., answering Q5 before Q4 in
one_at_a_time).
- Files: clients/web/src/components/math/KatexExpression.tsx:51, clients/web/src/components/editor/MathInsertPopover.tsx:176
- Issue: Raw
dangerouslySetInnerHTMLon KaTeX-rendered HTML with no DOMPurify pass. KaTeX withtrust: trueor with a future CVE could emit attacker-controlled HTML from student-authored LaTeX. - Risk: Stored XSS via quiz answers / question content rendered to graders or other students.
- Fix: Run all KaTeX output through
DOMPurify.sanitize(html, { USE_PROFILES: { mathMl: true, svg: true, html: true } }). Confirm KaTeX is called withtrust: false, strict: 'ignore'.
- File: clients/web/src/components/syllabus/SyllabusMarkdownView.tsx
- Issue:
react-markdown+rehype-katexrendered without an explicitrehype-sanitizeschema. Raw HTML in markdown is disabled by default in react-markdown, but the KaTeXrehype-raw/rehype-katexpipeline can reintroduce unsafe output if plugins change. - Risk: Any future config that enables raw HTML becomes a stored-XSS vector.
- Fix: Pin a
rehype-sanitizestep at the end of the pipeline with a schema that allows KaTeX classes only. Add a regression test that injects<img src=x onerror=alert(1)>and asserts it is stripped.
- File: server/internal/service/authservice/credentials_password.go
- Issue: Failed password checks return silently; no structured log emitted.
- Risk: Brute force and credential stuffing are undetectable from logs.
- Fix: Emit
slog.Warn("failed_login", "email", email, "remote_ip", remoteIP)on bad password and on unknown-email signup collision. Feed logs into a SIEM for alerting.
- File: server/internal/service/authservice/credentials_password.go
- Issue: Argon2 parameters match the previous Rust implementation defaults — not tuned for current hardware.
- Risk: Under-provisioned memory cost leaves hashes cheaper to crack than OWASP current guidance (19 MiB, t=2, p=1).
- Fix:
Benchmark on the target host and adjust until hashing takes ~250–500 ms.
params := &argon2id.Params{ Memory: 19 * 1024, Iterations: 2, Parallelism: 1, SaltLength: 16, KeyLength: 32, }
- File: server/internal/httpserver/module_quiz.go (
ModuleQuizSubmitHandler) - Issue: Unlimited submissions per user per quiz.
- Risk: Abuse of adaptive-quiz AI generation (cost), attempt count exhaustion DoS.
- Fix: Per (user, item) token bucket — e.g., 1 submission / 5 s, 20 / hour.
- File: server/internal/httpserver/ (multiple handlers)
- Issue: Course codes flow from URL path into logs, file paths, permission strings without length/charset check.
- Risk: Oversize or non-ASCII values pollute logs, permission strings, and filesystem segments.
- Fix: Reject anything not matching
^[A-Za-z0-9_-]{1,20}$at the top of every handler (or in a shared validation helper).
- File: server/internal/httpserver/cors.go — handled by H2 above; listed separately for tracking.
- File: server/internal/auth/jwt.go
- Issue: Single static signing key; no key-id (
kid) in header; rotating the secret invalidates every session. - Fix: Maintain a current + previous key, embed
kidin JWT header, verify against both during the overlap window. Store secrets in a vault (AWS Secrets Manager, Doppler, HashiCorp Vault).
- Files: server/internal/service/courseimageupload/service.go, server/internal/repos/coursestructure/
- Issue: Unexpected state panics the goroutine. Not a direct security bug but turns input anomalies into availability incidents.
- Fix: Replace panics with proper error returns and a dedicated error variant.
- Files: various in
server/internal/repos/ - Issue: Not injection (table names are internal constants), but the pattern is dangerous to extend. Future contributors may interpolate untrusted values.
- Fix: Use compile-time query constants where possible, or wrap dynamic identifiers through a whitelisting helper.
- Files: deploy configs
- Issue: No reverse-proxy redirect or HSTS policy documented.
- Fix: Document the production requirement: terminate TLS at the proxy, force 80→443, set HSTS preload. Add to deployment README.
- File: server/internal/apierr/apierr.go
- Fix: Emit
slog.Warn("permission_denied", "user_id", userID, "required_permission", perm, "route", route)from theForbiddenbranch. Aids forensics.
- Fix: Two-line additions to GitHub Actions. Fail the build on high-severity advisories. Enable Dependabot for both ecosystems.
- CSRF: cross-origin POST from fake origin returns 403.
- Rate limit: 10 failed logins/60s are throttled.
- XSS: stored quiz content with
<img src=x onerror=alert(1)>is escaped in the rendered DOM. - IDOR: student A's token cannot read
/api/v1/users/{studentB}/accommodations.
- Move JWT to
HttpOnlycookie (H1) - Implement CSRF + restrict CORS (H2)
- Add security headers (H3)
- Rate-limit auth endpoints (H4)
- Lock down file-upload path handling (H5)
- Close accommodations / quiz-results IDOR holes (M1, M2)
- Introduce dedicated gradebook-write permission (M3)
- Enforce quiz lockdown invariants server-side (M4)
- DOMPurify around all KaTeX / markdown render points (M5, M6)
- Failed-login and permission-denial audit logging (M7, I1)
- Rate-limit quiz submission (M9)
- Validate course-code input at the route boundary (M10)
- Tune Argon2 parameters (M8)
- JWT key rotation with
kid(L2) - Remove production panic paths (L3)
- Document HTTPS/HSTS deployment contract (L5)
- Add
govulncheck/npm auditto CI; enable Dependabot (I2) - Add automated security regression tests (I3)
- Refactor
fmt.Sprintf-based SQL table references (L4)
alexedwards/argon2id— current, goodgolang-jwt/jwt/v5— currentjackc/pgx/v5— current; parameterized queries in use throughoutgo-chi/chi/v5— currentgolang.org/x/crypto— current- Frontend: React 19.2, react-router 7.14, react-markdown + rehype-katex — current
- Missing on client:
dompurify. Add it as part of M5/M6 fix.
Run govulncheck ./... and npm audit on the current tree before merging the next release.
Every P0/P1 item should land with regression coverage:
- Unit tests where logic lives in a service module
- Integration tests in
server/test/for end-to-end auth, CSRF, IDOR, and rate-limit behavior - Component/DOM tests in the client for sanitizer regressions
A staging environment should be scanned with OWASP ZAP (or equivalent) before production cutover.