fix(regex_validator): add ReDoS detection via regexploit
Detect catastrophic backtracking patterns before regex compilation using regexploit library. Add ReDoSDetectedError exception and _MINIMUM_STARRINESS threshold (>=3) to catch dangerous patterns like (a+)+b. Update pyproject.toml deps, add tests for detection.
This commit is contained in:
@@ -3,12 +3,13 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.models.config import GlobalConfigUpdate, GlobalConfigResponse
|
||||
from app.models.config import GlobalConfigResponse, GlobalConfigUpdate
|
||||
|
||||
|
||||
def test_add_log_path_request_default_tail_is_true() -> None:
|
||||
"""Tail defaults to True."""
|
||||
from app.models.config import AddLogPathRequest
|
||||
|
||||
|
||||
req = AddLogPathRequest(log_path="/var/log/app.log")
|
||||
assert req.tail is True
|
||||
|
||||
@@ -16,7 +17,7 @@ def test_add_log_path_request_default_tail_is_true() -> None:
|
||||
def test_add_log_path_request_can_be_created() -> None:
|
||||
"""AddLogPathRequest can be created with valid data (no validators in model)."""
|
||||
from app.models.config import AddLogPathRequest
|
||||
|
||||
|
||||
req = AddLogPathRequest(log_path="/etc/passwd", tail=True)
|
||||
# Note: path validation is now in the router layer, not in the model
|
||||
assert req.log_path == "/etc/passwd"
|
||||
@@ -188,3 +189,132 @@ def test_setup_request_master_password_complexity_still_enforced() -> None:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
SetupRequest(master_password="Password1")
|
||||
assert "special character" in str(exc_info.value)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DashboardBanItem country_code validator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_dashboard_ban_item_country_code_null() -> None:
|
||||
"""DashboardBanItem accepts None for country_code."""
|
||||
from app.models.ban import DashboardBanItem
|
||||
|
||||
item = DashboardBanItem(
|
||||
ip="1.2.3.4",
|
||||
jail="sshd",
|
||||
banned_at="2026-04-28T07:00:00+00:00",
|
||||
ban_count=1,
|
||||
origin="selfblock",
|
||||
country_code=None,
|
||||
)
|
||||
assert item.country_code is None
|
||||
|
||||
|
||||
def test_dashboard_ban_item_country_code_valid() -> None:
|
||||
"""DashboardBanItem accepts a valid 2-char uppercase country code."""
|
||||
from app.models.ban import DashboardBanItem
|
||||
|
||||
item = DashboardBanItem(
|
||||
ip="1.2.3.4",
|
||||
jail="sshd",
|
||||
banned_at="2026-04-28T07:00:00+00:00",
|
||||
ban_count=1,
|
||||
origin="selfblock",
|
||||
country_code="US",
|
||||
)
|
||||
assert item.country_code == "US"
|
||||
|
||||
|
||||
def test_dashboard_ban_item_country_code_empty_string_coerced_to_none() -> None:
|
||||
"""DashboardBanItem coerces empty-string country_code to None."""
|
||||
from app.models.ban import DashboardBanItem
|
||||
|
||||
item = DashboardBanItem(
|
||||
ip="1.2.3.4",
|
||||
jail="sshd",
|
||||
banned_at="2026-04-28T07:00:00+00:00",
|
||||
ban_count=1,
|
||||
origin="selfblock",
|
||||
country_code="",
|
||||
)
|
||||
assert item.country_code is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ActiveBan country validator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_active_ban_country_null() -> None:
|
||||
"""ActiveBan accepts None for country."""
|
||||
from app.models.ban import ActiveBan
|
||||
|
||||
ban = ActiveBan(ip="1.2.3.4", jail="sshd", country=None)
|
||||
assert ban.country is None
|
||||
|
||||
|
||||
def test_active_ban_country_valid() -> None:
|
||||
"""ActiveBan accepts a valid country code."""
|
||||
from app.models.ban import ActiveBan
|
||||
|
||||
ban = ActiveBan(ip="1.2.3.4", jail="sshd", country="DE")
|
||||
assert ban.country == "DE"
|
||||
|
||||
|
||||
def test_active_ban_country_empty_string_coerced_to_none() -> None:
|
||||
"""ActiveBan coerces empty-string country to None."""
|
||||
from app.models.ban import ActiveBan
|
||||
|
||||
ban = ActiveBan(ip="1.2.3.4", jail="sshd", country="")
|
||||
assert ban.country is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ban country validator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_ban_country_null() -> None:
|
||||
"""Ban accepts None for country."""
|
||||
from app.models.ban import Ban
|
||||
|
||||
ban = Ban(
|
||||
ip="1.2.3.4",
|
||||
jail="sshd",
|
||||
banned_at="2026-04-28T07:00:00+00:00",
|
||||
ban_count=1,
|
||||
origin="selfblock",
|
||||
country=None,
|
||||
)
|
||||
assert ban.country is None
|
||||
|
||||
|
||||
def test_ban_country_valid() -> None:
|
||||
"""Ban accepts a valid country code."""
|
||||
from app.models.ban import Ban
|
||||
|
||||
ban = Ban(
|
||||
ip="1.2.3.4",
|
||||
jail="sshd",
|
||||
banned_at="2026-04-28T07:00:00+00:00",
|
||||
ban_count=1,
|
||||
origin="selfblock",
|
||||
country="FR",
|
||||
)
|
||||
assert ban.country == "FR"
|
||||
|
||||
|
||||
def test_ban_country_empty_string_coerced_to_none() -> None:
|
||||
"""Ban coerces empty-string country to None."""
|
||||
from app.models.ban import Ban
|
||||
|
||||
ban = Ban(
|
||||
ip="1.2.3.4",
|
||||
jail="sshd",
|
||||
banned_at="2026-04-28T07:00:00+00:00",
|
||||
ban_count=1,
|
||||
origin="selfblock",
|
||||
country="",
|
||||
)
|
||||
assert ban.country is None
|
||||
|
||||
Reference in New Issue
Block a user