From ec9319853f5251c98bafb3d20562cf7789ee1701 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 9 Oct 2026 23:50:37 -0500 Subject: [PATCH] reject usernames made only of dots A username of "." or ".." passed validation, but browsers resolve those as path segments, so requests to /users/{username} never reached the API and the user could not be deleted or edited from the UI. User creation now rejects dot-only names in both the API and the create user dialog. --- frigate/api/auth.py | 2 +- frigate/test/http_api/test_http_users.py | 33 +++++++++++++++++++ .../components/overlay/CreateUserDialog.tsx | 2 +- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 frigate/test/http_api/test_http_users.py diff --git a/frigate/api/auth.py b/frigate/api/auth.py index e0090e32cd..5f4de5204f 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -912,7 +912,7 @@ def create_user( HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations config_roles = list(request.app.frigate_config.auth.roles.keys()) - if not re.match("^[A-Za-z0-9._]+$", body.username): + if not re.match(r"^(?!\.+$)[A-Za-z0-9._]+$", body.username): return JSONResponse(content={"message": "Invalid username"}, status_code=400) if body.role not in config_roles: diff --git a/frigate/test/http_api/test_http_users.py b/frigate/test/http_api/test_http_users.py new file mode 100644 index 0000000000..a964e7c0af --- /dev/null +++ b/frigate/test/http_api/test_http_users.py @@ -0,0 +1,33 @@ +"""Tests for user creation.""" + +from unittest.mock import MagicMock + +from frigate.models import User +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + +PASSWORD = "a-valid-password-123" + + +class TestCreateUser(BaseTestHttp): + def setUp(self): + super().setUp([User]) + self.app = super().create_app() + self.app.config_publisher = MagicMock() + + def _create(self, username: str): + with AuthTestClient(self.app) as client: + return client.post( + "/users", + json={"username": username, "password": PASSWORD, "role": "viewer"}, + ) + + def test_rejects_dot_only_usernames(self): + # browsers resolve these as URL path segments, so the per-user + # endpoints can never be reached for them + for username in (".", "..", "..."): + assert self._create(username).status_code == 400 + assert User.get_or_none(User.username == username) is None + + def test_accepts_username_containing_dots(self): + assert self._create("john.doe_1").status_code == 200 + assert User.get_or_none(User.username == "john.doe_1") is not None diff --git a/web/src/components/overlay/CreateUserDialog.tsx b/web/src/components/overlay/CreateUserDialog.tsx index 3b003f2611..0b4f09aa78 100644 --- a/web/src/components/overlay/CreateUserDialog.tsx +++ b/web/src/components/overlay/CreateUserDialog.tsx @@ -80,7 +80,7 @@ export default function CreateUserDialog({ user: z .string() .min(1, t("users.dialog.form.usernameIsRequired")) - .regex(/^[A-Za-z0-9._]+$/, { + .regex(/^(?!\.+$)[A-Za-z0-9._]+$/, { message: t("users.dialog.createUser.usernameOnlyInclude"), }), password: z