Files
tutortool/docs/tutortool_audit_perplexity_260502.md
2026-05-03 00:41:50 +02:00

20 KiB
Raw Permalink Blame History

Security & Best-Practices Audit for tutortool

Overview

  • Full-stack app: Rust 1.95 backend with Axum 0.8, SQLx 0.8 (SQLite), JWT auth; SvelteKit 2 + Svelte 5 + TypeScript 5 + Vite 8 + pnpm 9 frontend.
  • CI/CD: Gitea Actions-style workflows for CI (PRs/branches) and Release (tag push), including Rust checks, frontend checks, Playwright E2E, cargo-audit, Docker build, Helm deploy to Kubernetes.
  • Overall: architecture is solid and modern; most obvious footguns are avoided, but there are some security and hardening issues plus a few best-practice gaps in both backend and frontend.

Toolchain & Dependencies

Backend (Rust)

  • Toolchain:
    • edition = "2024", rust-version = "1.95.0" pinned in backend/Cargo.toml and CI (dtolnay/rust-toolchain@master toolchain: '1.95.0').
  • Core crates:
    • axum 0.8 (web framework, with macros, multipart).
    • axum-extra 0.10 (cookies, etc.).
    • tokio 1 with full feature.
    • sqlx 0.8 with sqlite, runtime-tokio, macros, migrate.
    • serde/serde_json.
    • jsonwebtoken 10 (JWT handling, rust_crypto backend).
    • bcrypt 0.19 (password hashing).
    • tower-http 0.6 with fs, cors, trace.
    • tower_governor 0.6 (rate limiting).
    • chrono 0.4 with serde, rand 0.9, thiserror 2, tracing 0.1, tracing-subscriber 0.3.
  • Dev-deps:
    • tower 0.5 (util), http-body-util 0.1, bytes 1, temp-env 0.3, serial_test 3.1.
  • The combination (Axum + SQLx + jsonwebtoken) is a common pattern; community examples like Axium and blog posts promote similar stacks for high-performance, security-focused APIs.[^1][^2][^3]

Frontend (SvelteKit + TS)

  • Toolchain:
    • SvelteKit 2 (@sveltejs/kit ^2.59.0), Svelte ^5.55.5, Vite ^8.0.10, TypeScript ^5, @typescript/native-preview ^7.0.0-dev, pnpm 9.
    • Playwright @playwright/test ^1.59.1 for E2E; svelte-check for type+template checking.
  • This matches current Svelte best-practices: Vite-based tooling, svelte-check, Playwright, TS everywhere.[^4][^5]

CI/CD

  • CI workflow (.gitea/workflows/ci.yml):
    • Runs on push (non-main), and PRs.
    • Steps:
      • Node 22 + pnpm 9.
      • Rust 1.95 + clippy + rustfmt.
      • Cache Cargo and pnpm store.
      • pnpm --dir frontend install --frozen-lockfile.
      • svelte-kit sync, Playwright browser install.
      • Backend: cargo check, cargo clippy -D warnings, cargo fmt --check, cargo test.
      • Frontend: tsgo --version (from @typescript/native-preview) and pnpm check.
      • cargo audit with ignore RUSTSEC-2023-0071.
      • Frontend build.
      • E2E tests via make test-up + pnpm test:e2e, then teardown with make test-down.
      • Docker build (no push).
  • Release workflow (.gitea/workflows/release.yml):
    • Triggered on tag v*.*.*.
    • Re-runs checks, tests, cargo-audit, build.
    • Docker build+push to registry.itsh.dev/s0wlz/tutortool with latest and tag, login via secrets.
    • Installs kubectl config from base64-encoded secret, sets up Helm 3.16, and runs helm upgrade --install into namespace tenant-5 with values_override.yaml and image.tag from tag.
  • This aligns with modern GitHub/Gitea workflows: pinned major versions for actions, caching, separate CI and release pipelines, and Helm-based K8s deployment.[^6]

Backend Best-Practices Review

Axum / App Setup

  • AppState holds SqlitePool, jwt_secret, and test_mode, and implements FromRef for the pool, matching ergonomic Axum+SQLx patterns.[^2]
  • Middleware:
    • TraceLayer::new_for_http() is enabled to log requests; static assets served via ServeDir with SPA-style fallback to index.html.
    • Rate limiting (tower_governor) appears configured in routes::build (not shown in excerpt but implied by dependency choice).
  • Ports and bindings:
    • Binds to 0.0.0.0:PORT (default 3000) and serves over plain HTTP; this is expected behind a reverse proxy/ingress, but in prod TLS termination should happen at the edge.

Findings & Suggestions

  • Add graceful shutdown: hook into axum::serve with a shutdown signal to support rolling updates and avoid dropping in-flight requests.[^7]
  • Ensure tower_governor is applied to all state-changing routes (auth, check-in, etc.) to mitigate brute force; current routes module likely does this, but it is worth verifying per-route.[^1]

Error Handling

  • There is a dedicated error.rs and AppError type (pattern recommended by Axum guides): single error type, ? operator, mapping to HTTP responses.[^7]
  • This is aligned with modern Rust error-handling best practices: central error enum plus thiserror for derive and automatic conversions.[^7]

Findings & Suggestions

  • Confirm that AppError::Unauthorized and other variants do not leak internals (e.g., raw SQLx errors) in HTTP responses, and that detailed error messages go only to logs (tracing). This is in line with OWASP guidance on not exposing sensitive error details.[^8]

SQLx / Database Access

  • AppState owns the SqlitePool, consistent with SQLx ergonomics for Axum.[^2]
  • SQLx with sqlite feature uses libsqlite3 under the hood and introduces some unsafe, but SQLx forbids unsafe by default for other backends; that trade-off is known and accepted for SQLite.[^3]

Findings & Suggestions

  • Use fully parameterized queries everywhere, avoiding dynamic string concatenation; this matches SQLx and OWASP recommendations to prevent injection.[^9][^2]
  • Use migrations consistently (sqlx::migrate!()) in db::init() and ensure the CI includes a sqlx migrate run --check equivalent (offline) to prevent drift between schema and code.[^3]

JWT Handling (Authentication)

  • Claims structure:
    • TutorClaims { sub: i64, email: String, is_superadmin: bool, exp: u64 }.
  • Encoding:
    • encode_jwt sets exp to now + 7 days, uses Header::default() (HS256) and EncodingKey::from_secret(secret.as_bytes()).
  • Decoding:
    • decode_jwt uses DecodingKey::from_secret(secret.as_bytes()) and Validation::default(); errors map to AppError::Unauthorized and are traced at debug level.
  • Extraction:
    • Custom Axum extractor FromRequestParts for TutorClaims:
      • Tries CookieJar for "token" first (HttpOnly cookie expected from server) and falls back to Authorization: Bearer <token> header.
      • Uses AppState::from_ref to access jwt_secret and calls decode_jwt.
  • This pattern (HttpOnly cookie + optional Bearer header) is consistent with modern JWT auth designs, where cookies mitigate XSS theft of tokens and headers support scripting and tools.[^10][^8]

Findings (JWT)

  • Validation::default() only enforces expiration but uses default algorithm and leeway settings; explicitly setting algorithms and validate_exp is recommended to avoid alg downgrade issues and to be resilient to changes in defaults.[^11][^12]
  • exp is 7 days; OWASP and many JWT security guides recommend short-lived access tokens (1560 minutes) with refresh tokens if you need long-lived sessions.[^8][^10]
  • No audience (aud), issuer (iss), or other context claims are validated; for multi-tenant or multi-client deployments, those should be set and verified.[^8]

Suggestions (JWT)

  • Configure Validation explicitly:
    • Restrict algorithms (e.g., HS256 only) and disable validate_nbf if not used, but keep validate_exp on.[^12][^11]
    • Optionally validate iss and aud.
  • Consider split token model:
    • Short-lived access token in memory; long-lived refresh token in HttpOnly cookie as recommended by modern JWT best-practice guides.[^10]
  • Consider moving email out of the token or keeping only user id + roles; JWT best-practice docs recommend storing only non-sensitive, strictly necessary data.[^13]

Password Hashing

  • Uses bcrypt crate (0.19). Bcrypt is widely used and still acceptable, but many modern Rust security boilerplates (e.g. Axium) prefer Argon2id (memory-hard, OWASP recommended).[^1][^10]

Findings & Suggestions

  • If passwords are currently hashed with bcrypt, consider migrating to Argon2id for new deployments and implementing lazy rehash on login to avoid immediate full migration.[^1]
  • Ensure appropriate work factor/cost is configured (bcrypt default cost may be low for 2026 hardware; OWASP recommends tuning to ~250 ms per hash on your hardware).[^10]

Token & Secrets Management

  • jwt_secret is loaded from JWT_SECRET env var; app panics if missing (expect("JWT_SECRET must be set")).
  • CI and Release use cargo audit and Docker with registry login; K8s kubeconfig is passed via base64 secret into ~/.kube/config, and image registry credentials are from REGISTRY_USER and REGISTRY_TOKEN secrets.

Findings & Suggestions

  • Ensure JWT_SECRET is strong (at least 256 bits of randomness) and rotated periodically, as recommended in JWT and OWASP guidelines.[^8][^10]
  • Use kube secret and Helm values files for database credentials, SMTP, etc.; avoid ever committing real secrets (current repo appears clean of obvious .env/secrets, matching typical TS/Rust security guidance).[^9]

Test Mode & Test Reset Endpoint

  • In debug builds, TT_TEST_MODE=1 enables test-only behavior:
    • Loads demo/demo_seed.sql into routes::test_reset::SEED_SQL.
    • Logs warning TT_TEST_MODE active — /__test__/reset is enabled.
    • routes::build likely wires /__test__/reset route guarded by test_mode.
  • CI E2E flow sets TT_TEST_PORT_RANDOM=1 and uses make test-up to start backend; expected pattern is that test mode is enabled only in CI/dev.

Findings & Suggestions

  • Confirm that TT_TEST_MODE is never set in production environments; the log warning is helpful, but run-time checks or fail-fast on TT_TEST_MODE=1 in release builds would add extra safety.[^8]
  • The test reset endpoint should be fully disabled or return 404 in production; given architecture, this is likely already the case but should be validated in routing code.[^7]

Frontend Best-Practices Review

SvelteKit / Vite / TS Tooling

  • Scripts:
    • dev, build, preview for Vite.
    • check and check:watch using svelte-check with tsconfig.json.
    • test:e2e and test:e2e:ui using Playwright.
  • Config:
    • svelte.config.js and vite.config.ts present; playwright.config.ts configured for tests.
  • This aligns with Svelte docs: use svelte-check, Vite, and a linter for robustness.[^5][^4]

Findings & Suggestions

  • Consider adding ESLint with TypeScript+Svelte plugin to catch additional issues that TypeScript itself cannot (e.g., potential XSS sinks, unused variables).[^4][^9]
  • Ensure CSP and other security headers are set at the backend/Ingress level, especially for inline script blocking and stronger XSS mitigation, matching Svelte and TS security recommendations.[^5][^9]

TypeScript Practices

  • Uses typescript ^5 and @typescript/native-preview ^7.0.0-dev, with CI step tsgo --version && pnpm check, indicating use of the new TS native compiler experiment.
  • Modern TS security guidance emphasizes strict typing, avoiding any/unchecked casts, and runtime validation of external data.[^9]

Findings & Suggestions

  • Ensure tsconfig.json has strict flags (strict, noImplicitAny, noUncheckedIndexedAccess where feasible) to align with TS security best practices.[^9]
  • For input forms and API responses, combine TypeScript types with runtime validation (e.g. Zod) where user or external data is processed, as recommended in recent TS security articles.[^9]

Frontend Auth & Token Storage

  • The backend issues JWTs intended to be stored primarily in HttpOnly token cookie and optionally in Authorization header.
  • Modern JWT security guidance suggests storing access tokens in memory with refresh tokens in HttpOnly cookies to balance CSRF and XSS risks.[^10]

Findings & Suggestions

  • Ensure frontend never writes the JWT token to localStorage or sessionStorage; prefer HttpOnly cookies and/or in-memory access tokens per JWT security checklists.[^10]
  • Use SameSite=Lax or Strict plus Secure flag on cookies; backend should set these flags to mitigate CSRF and cookie theft, as recommended in TS+web security guides.[^9][^10]

CI/CD & Testing Review

CI Pipeline

  • Test job (CI):
    • Backend coverage: type check, Clippy with -D warnings, fmt check, unit tests, cargo audit with one specific advisory ignored (RUSTSEC-2023-0071), and Docker build.
    • Frontend coverage: pnpm install, svelte-kit sync, Playwright browser install, TypeScript check via tsgo and svelte-check, build, Playwright E2E tests against backend brought up via make test-up.
    • Failure path uploads Playwright artifacts for debugging.

Findings & Suggestions

  • Ignoring RUSTSEC-2023-0071 should be justified in the repo (e.g., README/SECURITY.md note) to document risk acceptance; OWASP and RustSec guidelines recommend handling advisories explicitly rather than silently ignoring them.[^3]
  • Consider adding cargo audit --deny-warnings in CI once all advisories are resolved to prevent new vulnerabilities from creeping in.[^3]
  • Add pnpm audit or npm audit equivalent carefully (with allowlist where necessary) to monitor JS dependency CVEs, aligning with TS and frontend security best practices.[^9]

Release Pipeline

  • Re-runs critical checks before building and pushing image and deploying via Helm; uses tag name as image tag and also pushes latest.

Findings & Suggestions

  • Using latest tag is convenient but can obscure which version is actually running; many release engineering guides recommend avoiding latest in production and relying on immutable tags only.[^6]
  • Helm upgrade uses --wait --timeout 5m, which is good; consider adding health/liveness/readiness probes in the Helm chart (if not already defined) to allow Kubernetes to verify app health before rollout completes.[^6]

Security Audit Summary

Strengths

  • Modern Rust backend stack (Axum, SQLx, jsonwebtoken, bcrypt) with clear error handling and tracing.[^3]
  • JWT usage is structured; tokens carry minimal data (id, email, role, exp) and are injected into handlers via typed Axum extractor.
  • CI pipeline is extensive: type checks, linting, formatting, unit tests, E2E tests, cargo-audit, Docker build.
  • Release pipeline uses Helm and secrets for registry and kubeconfig, with environment variables for image/namespace configuration.

Issues & Risks

  • JWT validation uses default Validation, not explicitly restricting algorithms or confirming iss/aud, which is discouraged by JWT crate best-practice discussions.[^11][^12]
  • Token lifetime is 7 days; guidance from JWT and OAuth security resources recommends much shorter access tokens with refresh token rotation.[^8][^10]
  • bcrypt is acceptable but no longer state-of-the-art; Argon2id is generally recommended for new password hashing deployments.[^1][^10]
  • CI ignores RUSTSEC-2023-0071 without a documented rationale in code; ignoring advisories without documentation is flagged as bad practice in security tooling docs.[^3]
  • No visible JS dependency audit step; frontend best-practice checklists call for regular dependency scanning.[^9]
  • Cookie flags (HttpOnly, SameSite, Secure) and CSRF protection are not visible from backend code snippet; recommended by TS/web security checklists.[^10][^9]
  • latest Docker tag usage in release; release engineering guides generally recommend immutable tags only.[^6]
  1. Harden JWT validation
    • Explicitly set allowed algorithms, enable validate_exp, and consider adding aud/iss checks.[^12][^11]
    • Consider reducing access token lifetime and introducing refresh tokens.
  2. Improve password hashing posture
    • Plan migration path to Argon2id for new passwords and rehash on login; keep bcrypt verification for legacy hashes.[^1][^10]
  3. Document and re-evaluate cargo audit ignore
    • Add a comment or SECURITY.md entry explaining the risk of RUSTSEC-2023-0071 and why it is acceptable, and track upstream fix to eventually drop the ignore.[^3]
  4. Add JS dependency scanning in CI
    • Use pnpm audit or an external scanner (e.g., snyk) with a curated allowlist.[^9]
  5. Cookie & CSRF Hardening
    • Ensure JWT cookies use HttpOnly, Secure, and SameSite=Lax/Strict flags and that state-changing endpoints enforce CSRF protections where relevant.[^10][^9]
  6. Release Tagging Improvements
    • Consider dropping latest tag in production and relying solely on versioned tags; ensure Helm values/overrides always reference immutable tags.[^6]
  7. Operational Safeguards for Test Mode
    • Enforce that TT_TEST_MODE cannot be set in production (e.g., check an env like ENV=prod and panic if both are set) to guarantee that /__test__/reset never exists in prod.[^7][^8]

These changes would bring the project in line with current Rust 1.95, SvelteKit, TypeScript, and JWT security best practices, while preserving its already solid architecture and testing setup.[^5][^8]


References

  1. Riktastic/Axium: An example API built with Rust, Axum, SQLx, and ... - Axium is a high-performance, security-focused API boilerplate built using Rust, Axum, SQLx, S3, Redi...

  2. An ergonomic pattern for SQLx queries in Axum - Joshka.net - In this post, I'll show an easy and ergonomic pattern for connecting to a database using SQLx and Ax...

  3. Building a REST API with Axum + Sqlx - I started to use Axum a few weeks ago, honestly, I'm a fan of the framework, so I'm writing this...

  4. How To Best Use Typescript for Props In Svelte 5 Project (VS Code)? - I am relatively new to TypeScript and am starting to add it into a Svelte 5 project. For a proof of ...

  5. Best practices • Svelte Docs - This document outlines some best practices that will help you write fast, robust Svelte apps. It is ...

  6. Migrating 8 SvelteKit Sites to Vite 8 in a day: What We Learned - If your SvelteKit guide references rollupOptions , update those references to rolldownOptions . And ...

  7. Building REST APIs with Rust and Axum: A Practical Beginner's Guide - Learn how to build fast, safe REST APIs using Rust and the Axum web framework. This step-by-step gui...

  8. Rust App Security: Master OAuth 2.0 & JWT - Secure your Rust applications with OAuth 2.0 and JWT! Learn step-by-step implementation for robust a...

  9. Typescript Application Security from A to Z: A Guide to Protecting ... - This article specifically provides simplified attack methods and vulnerability examples to make it e...

  10. Password Hashing - Learn how to implement secure JWT authentication in Rust applications. This guide covers token gener...

  11. Validate JWT using RS384 in Rust - SSOJet - Validate JWTs with RS384 in Rust. Secure your APIs by verifying token signatures efficiently and rel...

  12. JSON Web Token -- some investigative studies on crate ... - Regarding crate jsonwebtoken, the primary question is still how to check if a token is still valid,....

  13. Writing my first Rust crate: jsonwebtoken - Experience writing a JWT library in Rust