20 KiB
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 inbackend/Cargo.tomland CI (dtolnay/rust-toolchain@master toolchain: '1.95.0').
- Core crates:
axum 0.8(web framework, withmacros,multipart).axum-extra 0.10(cookies, etc.).tokio 1withfullfeature.sqlx 0.8withsqlite,runtime-tokio,macros,migrate.serde/serde_json.jsonwebtoken 10(JWT handling,rust_cryptobackend).bcrypt 0.19(password hashing).tower-http 0.6withfs,cors,trace.tower_governor 0.6(rate limiting).chrono 0.4withserde,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.1for E2E;svelte-checkfor type+template checking.
- SvelteKit 2 (
- 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) andpnpm check. cargo auditwith ignoreRUSTSEC-2023-0071.- Frontend build.
- E2E tests via
make test-up+pnpm test:e2e, then teardown withmake 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/tutortoolwithlatestand tag, login via secrets. - Installs kubectl config from base64-encoded secret, sets up Helm 3.16, and runs
helm upgrade --installinto namespacetenant-5withvalues_override.yamlandimage.tagfrom tag.
- Triggered on 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
AppStateholdsSqlitePool,jwt_secret, andtest_mode, and implementsFromReffor the pool, matching ergonomic Axum+SQLx patterns.[^2]- Middleware:
TraceLayer::new_for_http()is enabled to log requests; static assets served viaServeDirwith SPA-style fallback toindex.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.
- Binds to
Findings & Suggestions
- Add graceful shutdown: hook into
axum::servewith a shutdown signal to support rolling updates and avoid dropping in-flight requests.[^7] - Ensure
tower_governoris 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.rsandAppErrortype (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
thiserrorfor derive and automatic conversions.[^7]
Findings & Suggestions
- Confirm that
AppError::Unauthorizedand 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
AppStateowns theSqlitePool, consistent with SQLx ergonomics for Axum.[^2]- SQLx with
sqlitefeature 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!()) indb::init()and ensure the CI includes asqlx migrate run --checkequivalent (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_jwtsetsexpto now + 7 days, usesHeader::default()(HS256) andEncodingKey::from_secret(secret.as_bytes()).
- Decoding:
decode_jwtusesDecodingKey::from_secret(secret.as_bytes())andValidation::default(); errors map toAppError::Unauthorizedand are traced at debug level.
- Extraction:
- Custom Axum extractor
FromRequestPartsforTutorClaims:- Tries
CookieJarfor"token"first (HttpOnly cookie expected from server) and falls back toAuthorization: Bearer <token>header. - Uses
AppState::from_refto accessjwt_secretand callsdecode_jwt.
- Tries
- Custom Axum extractor
- 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 settingalgorithmsandvalidate_expis recommended to avoid alg downgrade issues and to be resilient to changes in defaults.[^11][^12]expis 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
Validationexplicitly:- Restrict algorithms (e.g., HS256 only) and disable
validate_nbfif not used, but keepvalidate_expon.[^12][^11] - Optionally validate
issandaud.
- Restrict algorithms (e.g., HS256 only) and disable
- 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
emailout 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
bcryptcrate (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_secretis loaded fromJWT_SECRETenv var; app panics if missing (expect("JWT_SECRET must be set")).- CI and Release use
cargo auditand Docker with registry login; K8s kubeconfig is passed via base64 secret into~/.kube/config, and image registry credentials are fromREGISTRY_USERandREGISTRY_TOKENsecrets.
Findings & Suggestions
- Ensure
JWT_SECRETis 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=1enables test-only behavior:- Loads
demo/demo_seed.sqlintoroutes::test_reset::SEED_SQL. - Logs warning
TT_TEST_MODE active — /__test__/reset is enabled. routes::buildlikely wires/__test__/resetroute guarded bytest_mode.
- Loads
- CI E2E flow sets
TT_TEST_PORT_RANDOM=1and usesmake test-upto start backend; expected pattern is that test mode is enabled only in CI/dev.
Findings & Suggestions
- Confirm that
TT_TEST_MODEis never set in production environments; the log warning is helpful, but run-time checks or fail-fast onTT_TEST_MODE=1in 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,previewfor Vite.checkandcheck:watchusingsvelte-checkwithtsconfig.json.test:e2eandtest:e2e:uiusing Playwright.
- Config:
svelte.config.jsandvite.config.tspresent;playwright.config.tsconfigured 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 ^5and@typescript/native-preview ^7.0.0-dev, with CI steptsgo --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.jsonhas strict flags (strict,noImplicitAny,noUncheckedIndexedAccesswhere 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
tokencookie and optionally inAuthorizationheader. - 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
localStorageorsessionStorage; prefer HttpOnly cookies and/or in-memory access tokens per JWT security checklists.[^10] - Use
SameSite=LaxorStrictplusSecureflag 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 auditwith one specific advisory ignored (RUSTSEC-2023-0071), and Docker build. - Frontend coverage: pnpm install, svelte-kit sync, Playwright browser install, TypeScript check via
tsgoandsvelte-check, build, Playwright E2E tests against backend brought up viamake test-up. - Failure path uploads Playwright artifacts for debugging.
- Backend coverage: type check, Clippy with
Findings & Suggestions
- Ignoring
RUSTSEC-2023-0071should 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-warningsin CI once all advisories are resolved to prevent new vulnerabilities from creeping in.[^3] - Add
pnpm auditornpm auditequivalent 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
latesttag is convenient but can obscure which version is actually running; many release engineering guides recommend avoidinglatestin 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 confirmingiss/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-0071without 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]
latestDocker tag usage in release; release engineering guides generally recommend immutable tags only.[^6]
Recommended Actions (Prioritized)
- Harden JWT validation
- Explicitly set allowed algorithms, enable
validate_exp, and consider addingaud/isschecks.[^12][^11] - Consider reducing access token lifetime and introducing refresh tokens.
- Explicitly set allowed algorithms, enable
- Improve password hashing posture
- Plan migration path to Argon2id for new passwords and rehash on login; keep bcrypt verification for legacy hashes.[^1][^10]
- Document and re-evaluate
cargo auditignore- Add a comment or SECURITY.md entry explaining the risk of
RUSTSEC-2023-0071and why it is acceptable, and track upstream fix to eventually drop the ignore.[^3]
- Add a comment or SECURITY.md entry explaining the risk of
- Add JS dependency scanning in CI
- Use
pnpm auditor an external scanner (e.g., snyk) with a curated allowlist.[^9]
- Use
- Cookie & CSRF Hardening
- Ensure JWT cookies use
HttpOnly,Secure, andSameSite=Lax/Strictflags and that state-changing endpoints enforce CSRF protections where relevant.[^10][^9]
- Ensure JWT cookies use
- Release Tagging Improvements
- Consider dropping
latesttag in production and relying solely on versioned tags; ensure Helm values/overrides always reference immutable tags.[^6]
- Consider dropping
- Operational Safeguards for Test Mode
- Enforce that
TT_TEST_MODEcannot be set in production (e.g., check an env likeENV=prodand panic if both are set) to guarantee that/__test__/resetnever exists in prod.[^7][^8]
- Enforce that
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
-
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...
-
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...
-
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...
-
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 ...
-
Best practices • Svelte Docs - This document outlines some best practices that will help you write fast, robust Svelte apps. It is ...
-
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 ...
-
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...
-
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...
-
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...
-
Password Hashing - Learn how to implement secure JWT authentication in Rust applications. This guide covers token gener...
-
Validate JWT using RS384 in Rust - SSOJet - Validate JWTs with RS384 in Rust. Secure your APIs by verifying token signatures efficiently and rel...
-
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,....
-
Writing my first Rust crate: jsonwebtoken - Experience writing a JWT library in Rust