Refactor backend architecture and update documentation

- Add CSRF protection middleware implementation
- Update API client with improved configuration
- Enhance documentation for backend development
- Add architecture documentation updates
- Reorganize and clean up task documentation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-26 14:52:23 +02:00
parent a44f1ef35b
commit c2348d7075
9 changed files with 470 additions and 66 deletions

View File

@@ -675,8 +675,21 @@ BanGUI maintains its **own SQLite database** (separate from the fail2ban databas
- **Runtime state** (`RuntimeState` in `app.utils.runtime_state`) — stores mutable application state: `server_status` (fail2ban online/offline), `last_activation` (jail activation tracking), `pending_recovery` (crash detection), and `runtime_settings` (effective configuration). **⚠️ RuntimeState is process-local and only safe when BanGUI runs as a single asyncio worker.** Mutations must not span `await` points (cooperative scheduling within a single event loop is safe). In multi-worker deployments, each process has its own copy — logouts from worker A don't affect worker B's cache, health status updates are per-worker, and activation tracking is unreliable. BanGUI enforces single-worker mode (TASK-002) to prevent this issue. For future multi-worker support, replace RuntimeState with a shared coordination backend (Redis, shared memory, database). See `app/utils/runtime_state.py` module docstring for details.
- **Setup-completion flag** — once `is_setup_complete()` returns `True`, the result is stored in `app.state._setup_complete_cached`. The `SetupRedirectMiddleware` skips the DB query on all subsequent requests, removing 1 SQL query per request for the common post-setup case. The completion flag is only written after the runtime database is successfully initialized and all initial setup settings are persisted, preventing a failed setup from permanently bypassing the setup wizard.
---
### 6.1 CSRF Protection
State-mutating endpoints (POST, PUT, DELETE, PATCH) that use cookie-based authentication are protected against Cross-Site Request Forgery (CSRF) attacks via a **custom header check middleware**.
**Design:**
- For requests authenticated via the session cookie (not Bearer token), the `CsrfMiddleware` requires the custom header `X-BanGUI-Request: 1` to be present.
- The frontend API client automatically includes this header on all requests.
- Cross-site `fetch()` calls cannot set custom headers without CORS preflight, which the backend rejects for non-allowed origins, providing defense-in-depth.
- Safe HTTP methods (GET, HEAD, OPTIONS) bypass the check.
- Bearer token authentication (via `Authorization: Bearer` header) bypasses the check because tokens are not CSRF-vulnerable (they are not automatically sent on cross-origin requests).
- Requests missing the CSRF header receive a `403 Forbidden` response with detail: `"CSRF validation failed. Request rejected."`.
This mechanism complements the existing `SameSite=Lax` cookie policy, which blocks traditional `<form>` POST requests but does not protect against JavaScript-initiated requests on a subdomain or same-origin XSS injection.
---
## 7. Scheduling
APScheduler 4.x (async mode) manages recurring background tasks.

View File

@@ -768,6 +768,50 @@ environment:
**Important:** If `Secure=true` is set, browsers will reject the session cookie when the backend is served over HTTP. Ensure your nginx/reverse proxy terminates TLS and passes `X-Forwarded-Proto: https` so FastAPI knows the connection is secure.
### CSRF Protection Middleware
State-mutating endpoints (POST, PUT, DELETE, PATCH) authenticated via session cookies are protected by the `CsrfMiddleware`, which enforces a custom header check.
**How It Works:**
1. For every request using a mutating HTTP method, the middleware checks:
- Is this request authenticated via session cookie (not Bearer token)?
- If yes, require the custom header `X-BanGUI-Request: 1`.
- If missing or incorrect, return `403 Forbidden`.
2. **Bearer token requests** (via `Authorization: Bearer` header) bypass the check because tokens are not CSRF-vulnerable — they are never automatically sent on cross-origin requests.
3. **Safe HTTP methods** (GET, HEAD, OPTIONS) bypass the check.
4. **Cross-site protection:** Cross-site JavaScript (`fetch()` calls from other origins) cannot set custom headers without CORS preflight, which the backend rejects for non-allowed origins. This provides defense-in-depth against subdomain attacks and XSS injection.
**Implementation Location:**
- Middleware: `backend/app/middleware/csrf.py`
- Registered in: `backend/app/main.py` via `app.add_middleware(CsrfMiddleware)`
**Example:**
```python
# ✓ Cookie-authenticated POST with CSRF header — allowed
POST /api/bans
Cookie: bangui_session=...
X-BanGUI-Request: 1
# ✗ Cookie-authenticated POST without CSRF header — rejected with 403
POST /api/bans
Cookie: bangui_session=...
(no X-BanGUI-Request header)
# ✓ Bearer token authentication without CSRF header — allowed
POST /api/bans
Authorization: Bearer <token>
(no X-BanGUI-Request header needed)
# ✓ Safe GET method without CSRF header — allowed
GET /api/jails
Cookie: bangui_session=...
(no X-BanGUI-Request header needed)
```
### fail2ban_start_command Configuration
The `fail2ban_start_command` setting specifies the shell command used to start the fail2ban daemon during recovery operations (e.g., after a rollback).

View File

@@ -1,67 +1,3 @@
## TASK-022 — Session tokens stored in plaintext in SQLite
**Severity:** High
### Where found
`backend/app/db.py``sessions` table schema: `token TEXT NOT NULL UNIQUE`. `backend/app/repositories/session_repo.py``INSERT INTO sessions (token, ...)` and `SELECT ... WHERE token = ?` both use the raw token value.
### Why this is needed
If the BanGUI SQLite database file is exposed (volume mount misconfiguration, backup leak, path traversal via another vulnerability), all active session tokens are immediately usable — no cracking required. The attacker can directly use the token in the `bangui_session` cookie or `Authorization: Bearer` header.
### Goal
Store a one-way hash of the session token in the database so that the DB file alone is not sufficient to hijack a session.
### What to do
1. In `session_repo.create_session()`, store `hashlib.sha256(token.encode()).hexdigest()` instead of `token` in the `token` column.
2. In `session_repo.get_session()` and `delete_session()`, hash the supplied token before the SQL lookup.
3. The `Session` model's `token` field returned to the service layer still contains the raw token (for use in signing and response) — only the DB column changes.
4. Add a migration (`_MIGRATIONS[2]`) that renames the existing `sessions` table to `sessions_old`, creates a new one, and drops `sessions_old` (or simply truncates all sessions on upgrade, since they are all compromised anyway once the DB was readable in plaintext).
### Possible traps and issues
- Coordinate with TASK-025 (HMAC bypass) — both fixes invalidate all existing sessions. Do them in the same release.
- The migration must be atomic (see TASK-023).
- The `Session.token` field name is slightly misleading once it stores a hash — consider renaming the DB column to `token_hash`.
### Docs changes needed
- `Architekture.md` — update session data model description.
- `Backend-Development.md` — document the session token hashing pattern.
### Doc references
- [Architekture.md](Architekture.md) — authentication and session model
- [Backend-Development.md](Backend-Development.md) — security patterns
---
## TASK-023 — Database migration is non-atomic
**Severity:** Medium
### Where found
`backend/app/db.py``_apply_migration()`: calls `db.executescript(migration_script)` (which auto-commits per SQLite Python driver behavior) and then separately `db.execute("INSERT INTO schema_migrations ...")` + `db.commit()`.
### Why this is needed
`executescript()` issues an implicit `COMMIT` before executing the script, so the schema change and the migration record insertion are in two separate transactions. A process crash between them leaves the database in a migrated-but-unrecorded state. On next startup, the migration is re-applied. For a migration that is not idempotent (e.g., `INSERT` without `OR IGNORE`, `ALTER TABLE ADD COLUMN` without `IF NOT EXISTS`), this causes a runtime error or data duplication.
### Goal
Wrap each migration's DDL and its `schema_migrations` record in a single atomic transaction.
### What to do
1. Replace `db.executescript(migration_script)` with individual `await db.execute(stmt)` calls for each DDL statement in the migration (split on `;`).
2. Wrap the entire migration (all DDL statements + the `INSERT INTO schema_migrations`) in an explicit `BEGIN IMMEDIATE` ... `COMMIT` transaction.
3. Test: verify that a simulated crash mid-migration (mocked `execute` that raises on the second statement) leaves the DB at its prior version.
### Possible traps and issues
- SQLite DDL in WAL mode: `CREATE TABLE IF NOT EXISTS` and `CREATE INDEX IF NOT EXISTS` are safe to re-run. `ALTER TABLE ADD COLUMN` is not — it must be guarded with a `PRAGMA table_info` check if used in future migrations.
- Splitting a migration script on `;` must handle semicolons inside string literals and comments. Consider storing each migration as a `list[str]` of individual statements instead of a single script string.
### Docs changes needed
- `Backend-Development.md` — migration authoring guidelines.
### Doc references
- [Backend-Development.md](Backend-Development.md) — database schema and migrations
---
## TASK-024 — No CSRF protection on state-mutating endpoints
**Severity:** High

View File

@@ -114,6 +114,19 @@ fetchBans(24, ctrl.signal) // Pass the signal to enable cancellation on unmount
.catch(err => { /* ... */ });
```
### CSRF Protection Header
All state-mutating requests (POST, PUT, DELETE, PATCH) automatically include the custom header `X-BanGUI-Request: 1` via the central API client. This protects against Cross-Site Request Forgery (CSRF) attacks by requiring a custom header that cross-site JavaScript cannot set without CORS preflight.
**How it works:**
- The `request()` function in `api/client.ts` includes `"X-BanGUI-Request": "1"` in the default headers.
- GET, HEAD, and OPTIONS requests are unaffected.
- Bearer token authentication bypasses the check (tokens are not CSRF-vulnerable).
- The backend `CsrfMiddleware` validates this header for cookie-authenticated state-mutating requests.
- Requests missing the header receive a `403 Forbidden` response.
**No Action Required:** As a developer, you do not need to manually add this header — the centralized API client handles it automatically. All `api.post()`, `api.put()`, `api.del()` calls will include it.
### Request Deduplication & Shared Caching
When multiple components mount simultaneously and need the same data, **implement shared hooks with request deduplication** to avoid duplicate API calls. Use a module-level cache to ensure all consumers share a single in-flight request: