mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
is_auth_enabled() and auth_middleware both caught every exception during
the auth-state probe and returned the "allow" answer instead of denying
the request. Reporter's PoC floods /api/v1/auth/login to exhaust file
descriptors, forcing the next SQLite connect to raise, then hits a
protected endpoint during the fail-open window with no token — granting
unauthenticated access to admin-account creation, API-key creation, DB
backup download, and printer control. CWE-636 / CWE-755. Affects >= 0.1.6.
Fix:
- is_auth_enabled (backend/app/core/auth.py): only returns False for the
legitimate "settings row absent" case; any actual exception propagates
so the caller can deny the request.
- auth_middleware (backend/app/main.py): returns 503 on any probe failure
instead of await call_next(request).
4 new regression tests in test_auth_fail_closed.py pin the contract
(propagates DB exceptions, returns False for no-row, True for "true",
False for "false"). 1 existing security test renamed and updated to
accept either 500 or 503 (both fail-closed) and to verify the
SQLAlchemy detail does not leak in the body.
Codebase grep confirmed no other auth-decision predicate has the same
fail-open shape: _validate_api_key returns None on catch (→ 401 fail-
closed downstream), is_advanced_auth_enabled propagates correctly,
permissions.py has no catch-alls.
Reported by @wondercrash via private advisory.
82 lines
2.9 KiB
Python
82 lines
2.9 KiB
Python
"""Regression tests for the GHSA-6mf4-q26m-47pv fail-open auth bypass.
|
|
|
|
The previous version of ``is_auth_enabled`` caught every exception and
|
|
returned False (auth disabled). An attacker could trigger a DB-side
|
|
exception — the documented PoC exhausts file descriptors via a flood on
|
|
``/api/v1/auth/login`` until the next SQLite ``connect`` raises — and then
|
|
hit any protected endpoint during that fail-open window with no token at
|
|
all. Severity CVSS 9.8.
|
|
|
|
These tests pin the fail-closed contract:
|
|
|
|
1. ``is_auth_enabled`` propagates any DB exception (instead of swallowing
|
|
it and returning False).
|
|
2. The "no settings row" path still returns False (auth was legitimately
|
|
never configured).
|
|
3. ``setting.value == "true"`` still returns True.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from backend.app.core.auth import is_auth_enabled
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_auth_enabled_propagates_db_exception_instead_of_failing_open():
|
|
"""The core regression for GHSA-6mf4-q26m-47pv. A DB error during the
|
|
auth-enabled probe must propagate — fail closed — instead of returning
|
|
False and treating the system as auth-disabled."""
|
|
|
|
db = AsyncMock()
|
|
db.execute = AsyncMock(side_effect=OSError("simulated file-descriptor exhaustion"))
|
|
|
|
with pytest.raises(OSError, match="simulated file-descriptor exhaustion"):
|
|
await is_auth_enabled(db)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_auth_enabled_returns_false_when_settings_row_absent():
|
|
"""Legitimate 'auth was never configured' path: the settings row simply
|
|
does not exist. ``scalar_one_or_none`` returns None, no exception, and
|
|
the function returns False — system is auth-disabled by configuration,
|
|
not because the DB blew up."""
|
|
|
|
result = MagicMock()
|
|
result.scalar_one_or_none = MagicMock(return_value=None)
|
|
db = AsyncMock()
|
|
db.execute = AsyncMock(return_value=result)
|
|
|
|
assert await is_auth_enabled(db) is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_auth_enabled_returns_true_when_setting_value_is_true():
|
|
"""Happy path: the settings row exists and its value is "true" → auth
|
|
is enabled and the caller must require credentials."""
|
|
|
|
setting = MagicMock()
|
|
setting.value = "true"
|
|
result = MagicMock()
|
|
result.scalar_one_or_none = MagicMock(return_value=setting)
|
|
db = AsyncMock()
|
|
db.execute = AsyncMock(return_value=result)
|
|
|
|
assert await is_auth_enabled(db) is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_auth_enabled_returns_false_when_setting_value_is_false():
|
|
"""Happy path: the settings row exists and its value is "false" → auth
|
|
is disabled by configuration (legitimate, not exception)."""
|
|
|
|
setting = MagicMock()
|
|
setting.value = "false"
|
|
result = MagicMock()
|
|
result.scalar_one_or_none = MagicMock(return_value=setting)
|
|
db = AsyncMock()
|
|
db.execute = AsyncMock(return_value=result)
|
|
|
|
assert await is_auth_enabled(db) is False
|