TASK-031: Enforce bcrypt 72-byte password limit

Bcrypt silently truncates passwords at 72 bytes, so passwords longer than 72
characters provide no additional security. This commit enforces the 72-byte
maximum across the authentication and setup flows.

Changes:
- Add max_length=72 to LoginRequest.password and SetupRequest.master_password
- Update field validator in SetupRequest to explicitly check max_length
- Add comprehensive tests for password length validation (6 new test cases)
- Document the 72-byte limitation in Features.md (master password options)
- Add new section 12 'Password Hashing' in Backend-Development.md explaining:
  - The bcrypt truncation behavior
  - Why the limit is enforced
  - The validation flow from frontend to backend
  - What happens when passwords exceed the limit

All existing tests pass, no regressions introduced.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-26 15:38:20 +02:00
parent 1d91e24a88
commit 32aad186c3
5 changed files with 121 additions and 7 deletions

View File

@@ -1085,7 +1085,39 @@ The login endpoint (`POST /api/auth/login`) is protected against brute-force att
---
## 13. File I/O Conventions
## 12. Password Hashing
The master password is hashed using **bcrypt** with an auto-generated salt. All password validation uses the models in `app.models.auth` and `app.models.setup`.
### The 72-Byte Bcrypt Limitation
**Important:** bcrypt silently truncates all input at **72 bytes** before hashing. This means:
- A user who sets a 100-character password is actually authenticated by only the first 72 bytes
- Extra characters beyond 72 bytes provide **zero additional security**
- An attacker who has reduced their search space to 72 bytes can brute-force the password more efficiently than intended
**Solution:** Both password fields enforce a **maximum length of 72 bytes**:
- `LoginRequest.password` — max 72 characters (enforced via Pydantic `Field(max_length=72)`)
- `SetupRequest.master_password` — max 72 characters (enforced via Pydantic `Field(max_length=72)`)
**Validation flow:**
1. Frontend → hashes password with SHA256 using `SubtleCrypto` before transmission
2. Backend receives SHA256 hash, validates length (≤ 72 bytes)
3. Backend → hashes with bcrypt using `run_blocking(bcrypt.hashpw)` to avoid event loop stall
4. Hash stored in SQLite `settings` table
**If a password exceeds 72 bytes:**
- Pydantic raises `ValidationError` with error code `string_too_long`
- The router returns **HTTP 422 Unprocessable Entity**
- The frontend should inform the user to choose a shorter password
**Implementation:**
- Models: `app.models.auth.LoginRequest`, `app.models.setup.SetupRequest`
- Service layer: `app.services.auth_service._check_password()`, `app.services.setup_service.run_setup()`
---
## 14. File I/O Conventions
All file write operations to critical configuration files must be **atomic** to prevent corruption if the process is killed mid-write.
@@ -1163,7 +1195,7 @@ atomic_write(path, updated_content) # Atomic write, auto-cleanup on error
---
## 14. Git & Workflow
## 15. Git & Workflow
- **Branch naming:** `feature/<short-description>`, `fix/<short-description>`, `chore/<short-description>`.
- **Commit messages:** imperative tense, max 72 chars first line (`Add jail reload endpoint`, `Fix ban history query`).
@@ -1173,7 +1205,7 @@ atomic_write(path, updated_content) # Atomic write, auto-cleanup on error
---
## 15. Coding Principles
## 16. Coding Principles
These principles are **non-negotiable**. Every backend contributor must internalise and apply them daily.
@@ -1560,7 +1592,7 @@ When user-supplied URLs are fetched by the backend, validate them before making
---
## 16. Quick Reference — Do / Don't
## 17. Quick Reference — Do / Don't
| Do | Don't |
|---|---|

View File

@@ -14,7 +14,7 @@ A web application to monitor, manage, and configure fail2ban from a clean, acces
### Options
- **Master Password** — Set a single global password that protects the entire web interface.
- **Master Password** — Set a single global password that protects the entire web interface. Must be between 8 and 72 characters long (72-byte limit is due to bcrypt truncation) and include one uppercase letter, one number, and one special character from `!@#$%^&*()`.
- **Database Path** — Define where the application stores its own SQLite database.
- **fail2ban Connection** — Specify how the application connects to the running fail2ban instance (socket path or related settings).
- **General Preferences** — Any additional application-level settings such as default time zone, date format, or session duration.