From 9c92d39629175aa0ad473a2dd29dc5ee4dc589fb Mon Sep 17 00:00:00 2001 From: Pouzor Date: Sun, 29 Mar 2026 16:03:40 +0200 Subject: [PATCH] fix: return 401 (not 500) when bcrypt hash is malformed (#21) - verify_password catches ValueError from passlib so a mangled hash ($ signs stripped by shell/Docker) returns False instead of crashing - Settings.check_password_hash logs a clear startup error with fix instructions when AUTH_PASSWORD_HASH doesn't start with '$2' --- backend/app/core/config.py | 15 +++++++++++++++ backend/app/core/security.py | 5 ++++- backend/tests/test_auth.py | 12 ++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 7d6a1c6..749382a 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,8 +1,12 @@ import json +import logging from pathlib import Path +from pydantic import model_validator from pydantic_settings import BaseSettings, SettingsConfigDict +logger = logging.getLogger(__name__) + class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") @@ -19,6 +23,17 @@ class Settings(BaseSettings): auth_username: str = "admin" auth_password_hash: str = "" + @model_validator(mode="after") + def check_password_hash(self) -> "Settings": + h = self.auth_password_hash + if h and not h.startswith("$2"): + logger.error( + "AUTH_PASSWORD_HASH looks invalid (does not start with '$2b$'). " + "bcrypt hashes contain '$' signs — wrap the value in single quotes " + "in your .env file: AUTH_PASSWORD_HASH='$2b$12$...'" + ) + return self + # Scanner scanner_ranges: list[str] = ["192.168.1.0/24"] diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 1cf7c04..ee9257a 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -9,7 +9,10 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def verify_password(plain: str, hashed: str) -> bool: - return bool(pwd_context.verify(plain, hashed)) + try: + return bool(pwd_context.verify(plain, hashed)) + except ValueError: + return False def hash_password(password: str) -> str: diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index 7f3f001..f65f578 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -56,3 +56,15 @@ async def test_service_key_disabled_when_not_configured(client: AsyncClient): settings.mcp_service_key = "" res = await client.get("/api/v1/nodes", headers={"X-MCP-Service-Key": "any-key"}) assert res.status_code == 401 + + +async def test_login_with_malformed_hash_returns_401_not_500(client: AsyncClient): + """Malformed hash (e.g. $ stripped by shell) must not crash with 500.""" + from app.core.config import settings + original = settings.auth_password_hash + settings.auth_password_hash = "2b12RtMbyw17l4N5UGzeXMNAWu" # $ signs stripped + try: + res = await client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"}) + assert res.status_code == 401 + finally: + settings.auth_password_hash = original