# 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 ` 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 (15–60 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] ### Recommended Actions (Prioritized) 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 ...](https://github.com/Riktastic/Axium) - 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](https://www.joshka.net/axum-sqlx-queries-pattern/) - 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](https://carlosmv.hashnode.dev/creating-a-rest-api-with-axum-sqlx-rust) - 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)?](https://www.reddit.com/r/sveltejs/comments/1i6igz0/how_to_best_use_typescript_for_props_in_svelte_5/) - 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](https://svelte.dev/docs/svelte/best-practices) - 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](https://cogley.jp/articles/migrating-sveltekit-to-vite-8) - 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](https://noqta.tn/en/tutorials/rust-axum-rest-api-beginner-guide-2026) - 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](https://codezup.com/securing-rust-oauth-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 ...](https://dev.to/devsdaddy/typescript-application-security-from-a-to-z-a-guide-to-protecting-against-obvious-and-55nh) - This article specifically provides simplified attack methods and vulnerability examples to make it e... 10. [Password Hashing](https://oneuptime.com/blog/post/2026-01-07-rust-jwt-authentication/view) - Learn how to implement secure JWT authentication in Rust applications. This guide covers token gener... 11. [Validate JWT using RS384 in Rust - SSOJet](https://ssojet.com/jwt-validation/validate-jwt-using-rs384-in-rust/) - 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 ...](https://dev.to/behainguyen/rust-json-web-token-some-investigative-studies-on-crate-jsonwebtoken-1mch) - Regarding crate jsonwebtoken, the primary question is still how to check if a token is still valid,.... 13. [Writing my first Rust crate: jsonwebtoken](https://www.vincentprouillet.com/blog/writing-my-first-crate/) - Experience writing a JWT library in Rust