From e3a4d4037c3fcc8b03fb6a7904f8d6fc8cb519fe Mon Sep 17 00:00:00 2001 From: olel Date: Sat, 28 Mar 2026 15:38:16 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=94=A7=20add=20config=20flag=20for=20?= =?UTF-8?q?username=20chars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config.py | 15 ++++++++------- docs/configuration.md | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index d14010d..a314f30 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -5,8 +5,9 @@ Licensed under the MIT license. See LICENSE file in the project root for details. """ -from logging import INFO, getLevelNamesMapping import secrets +import string +from logging import INFO, getLevelNamesMapping from typing import Annotated, Any, Literal from pydantic import ( @@ -17,9 +18,6 @@ ) from pydantic_core import MultiHostUrl from pydantic_settings import BaseSettings, SettingsConfigDict -from pydantic_extra_types.color import Color - -import string def parse_cors(v: Any) -> list[str] | str: @@ -46,6 +44,7 @@ class PublicSettings(BaseModel): ENABLE_PAGES: bool PAGES_TITLE: str LIMIT_REGISTRATION: bool + ALLOWED_USERNAME_CHARS: str ENABLE_WEBSIP: bool WEBSIP_PUBLIC: bool WEBSIP_EXTENSION_RANGE: tuple[int, int] @@ -73,6 +72,8 @@ class Settings(BaseSettings): ACCESS_TOKEN_EXPIRE_MINUTES: int = 11520 # 8 days LIMIT_REGISTRATION: bool = False + ALLOWED_USERNAME_CHARS: str = string.ascii_letters + string.digits + ## NETWORK WEB_PREFIX: str = "" API_V1_STR: str = "/api/v1" @@ -81,9 +82,9 @@ class Settings(BaseSettings): WEB_HOST: str = "127.0.0.1:8000" ASTERISK_HOST: str = "127.0.0.1" - BACKEND_CORS_ORIGINS: Annotated[list[AnyUrl] | str, BeforeValidator(parse_cors)] = ( - [] - ) + BACKEND_CORS_ORIGINS: Annotated[ + list[AnyUrl] | str, BeforeValidator(parse_cors) + ] = [] @computed_field # type: ignore[prop-decorator] @property diff --git a/docs/configuration.md b/docs/configuration.md index 1cd1aae..533a090 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -14,11 +14,12 @@ The credentials for the first created account are configured by ### Security -| Key | Description | Default | -| -------------------------------- | -------------------------------------- | ----------------- | -| UURU_SECRET_KEY | Secret key used to sign session tokens | random (32 chars) | -| UURU_ACCESS_TOKEN_EXPIRE_MINUTES | Minutes until a login expires | 8 days (11520) | -| UURU_BACKEND_CORS_ORIGINS | A list of allowed CORS origins | `[]` | +| Key | Description | Default | +| -------------------------------- | --------------------------------------------------- | ----------------- | +| UURU_SECRET_KEY | Secret key used to sign session tokens | random (32 chars) | +| UURU_ACCESS_TOKEN_EXPIRE_MINUTES | Minutes until a login expires | 8 days (11520) | +| UURU_BACKEND_CORS_ORIGINS | A list of allowed CORS origins | `[]` | +| UURU_ALLOWED_USERNAME_CHARS | A set of characters which are allowed for usernames | `A-Z,a-z,0-9` | ### HTTP routing @@ -145,14 +146,13 @@ The `UURU_WEBSIP_EXTENSION_RANGE` should be reserved via the `UURU_RESERVED_EXTE ### Asterisk Manager Interface !!! info - Because the `asterisk` container is exposed via `--network=host` we have to bind the AMI interface to a specific (non 127.0.0.0/8) ip. - We assume that your docker installation ships with `docker0` on `172.17.0.1` - change to your setup if needed. +Because the `asterisk` container is exposed via `--network=host` we have to bind the AMI interface to a specific (non 127.0.0.0/8) ip. +We assume that your docker installation ships with `docker0` on `172.17.0.1` - change to your setup if needed. !!! danger - By default all other containers/processes on your machine can talk via `AMI` to the `asterisk`. - We assume a trusted environment. - Please generate a secure `UURU_ASTERISK_AMI_PASS` for production setups. - +By default all other containers/processes on your machine can talk via `AMI` to the `asterisk`. +We assume a trusted environment. +Please generate a secure `UURU_ASTERISK_AMI_PASS` for production setups. | Key | Description | Default | | ---------------------- | ------------------------------------------- | -------------------------- | From f37a9cc49de8127f825a60eaa871a33451f9aee7 Mon Sep 17 00:00:00 2001 From: olel Date: Sat, 28 Mar 2026 15:43:44 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A6=BA=20check=20if=20username=20only?= =?UTF-8?q?=20contains=20valid=20chars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/security.py | 11 ++++++++--- app/models/crud/user.py | 8 +++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/core/security.py b/app/core/security.py index 293dcda..5325277 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -5,13 +5,14 @@ Licensed under the MIT license. See LICENSE file in the project root for details. """ +import random +import string from datetime import datetime, timedelta, timezone from typing import Any + +import jwt from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError -import jwt -import random -import string from app.core.config import settings @@ -58,3 +59,7 @@ def generate_peer_secret(): return "".join( random.choice(string.ascii_letters + string.digits) for _ in range(32) ) + + +def check_username(name: str) -> bool: + return set(name) <= set(settings.ALLOWED_USERNAME_CHARS) diff --git a/app/models/crud/user.py b/app/models/crud/user.py index 345af70..2594ade 100644 --- a/app/models/crud/user.py +++ b/app/models/crud/user.py @@ -11,7 +11,7 @@ from sqlmodel import Session, col, select from app.core.config import settings -from app.core.security import get_password_hash, verify_password +from app.core.security import check_username, get_password_hash, verify_password from app.models.crud import CRUDNotAllowedException from app.models.user import ( Invite, @@ -51,6 +51,9 @@ def create_user( if creating_user is None or creating_user.role != UserRole.ADMIN: raise CRUDNotAllowedException("You may not register an admin account") + if not check_username(new_user.username): + raise CRUDNotAllowedException("Username contains invalid characters!") + db_obj = User.model_validate( new_user, update={"password_hash": get_password_hash(new_user.password)} ) @@ -79,6 +82,9 @@ def update_user( ): raise CRUDNotAllowedException("You're not allowed to change this users role!") + if update_data.username is not None and not check_username(update_data.username): + raise CRUDNotAllowedException("Username contains invalid characters!") + data = update_data.model_dump(exclude_unset=True) extra_data = {} if "password" in data and data["password"] is not None: From 48f62e3450c65ec74512aa2f78954751ec510715 Mon Sep 17 00:00:00 2001 From: olel Date: Sat, 28 Mar 2026 17:09:21 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=A6=BA=20limit=20input=20to=20allowed?= =?UTF-8?q?=20characters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/client/types.gen.ts | 4 ++++ frontend/src/routes/register.svelte | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts index 7acba0b..3177a5d 100644 --- a/frontend/src/client/types.gen.ts +++ b/frontend/src/client/types.gen.ts @@ -829,6 +829,10 @@ export type PublicSettings = { * Limit Registration */ LIMIT_REGISTRATION: boolean; + /** + * Allowed Username Chars + */ + ALLOWED_USERNAME_CHARS: string; /** * Enable Websip */ diff --git a/frontend/src/routes/register.svelte b/frontend/src/routes/register.svelte index aabaf5c..734cc94 100644 --- a/frontend/src/routes/register.svelte +++ b/frontend/src/routes/register.svelte @@ -9,9 +9,8 @@ ModalBody, ModalFooter } from '@sveltestrap/sveltestrap'; - import { tick } from 'svelte'; - import { push_api_error, push_message } from '../messageService.svelte'; import { registerApiV1UserRegisterPost } from '../client'; + import { push_api_error, push_message } from '../messageService.svelte'; import { settings } from '../sharedState.svelte'; let { isOpen = $bindable() } = $props(); @@ -81,7 +80,7 @@
- +