All checks were successful
Release / release (push) Successful in 5m24s
286 lines
20 KiB
Markdown
286 lines
20 KiB
Markdown
# 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 (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
|
||
|