From d6458885e00788e1dec779b6807a79646aac6ed6 Mon Sep 17 00:00:00 2001 From: kj_sh604 Date: Fri, 3 Apr 2026 02:09:14 -0400 Subject: refactor: xss and sql injection security hardening --- auth_backend.py | 22 ++++- shim_app.py | 280 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 282 insertions(+), 20 deletions(-) diff --git a/auth_backend.py b/auth_backend.py index a8ad782..3902d1e 100644 --- a/auth_backend.py +++ b/auth_backend.py @@ -14,6 +14,9 @@ from typing import Callable, Optional, Protocol # we store encrypted challenge output instead of storing passwords. AUTH_CHALLENGE = "SHIM_AUTH_VALID" USERNAME_RE = re.compile(r"^[a-z0-9_.-]{2,64}$") +UUID_RE = re.compile( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" +) ConnectFn = Callable[[], sqlite3.Connection] @@ -39,6 +42,13 @@ def normalize_username(username: str) -> str: return username.strip().lower() +def normalize_uuid(value: str) -> Optional[str]: + candidate = (value or "").strip().lower() + if not UUID_RE.fullmatch(candidate): + return None + return candidate + + def looks_like_python_script(path: Path) -> bool: try: with open(path, "r", encoding="utf-8", errors="ignore") as f: @@ -93,6 +103,10 @@ class LocalMojicryptAuthBackend: return False, "username already exists" def update_username(self, user_uuid: str, new_username: str) -> tuple[bool, str]: + normalized_user_uuid = normalize_uuid(user_uuid) + if normalized_user_uuid is None: + return False, "invalid user id" + normalized = normalize_username(new_username) username_error = self._validate_username(normalized) if username_error: @@ -102,7 +116,7 @@ class LocalMojicryptAuthBackend: with self.connect_db() as conn: cursor = conn.execute( "UPDATE users SET username = ? WHERE user_uuid = ?", - (normalized, user_uuid), + (normalized, normalized_user_uuid), ) if cursor.rowcount == 0: return False, "user not found" @@ -112,6 +126,10 @@ class LocalMojicryptAuthBackend: return True, "username updated" def update_password(self, user_uuid: str, new_password: str) -> tuple[bool, str]: + normalized_user_uuid = normalize_uuid(user_uuid) + if normalized_user_uuid is None: + return False, "invalid user id" + password_error = self._validate_password(new_password) if password_error: return False, password_error @@ -124,7 +142,7 @@ class LocalMojicryptAuthBackend: with self.connect_db() as conn: cursor = conn.execute( "UPDATE users SET encrypted_challenge = ? WHERE user_uuid = ?", - (encrypted, user_uuid), + (encrypted, normalized_user_uuid), ) if cursor.rowcount == 0: return False, "user not found" diff --git a/shim_app.py b/shim_app.py index f7e68af..875e21b 100644 --- a/shim_app.py +++ b/shim_app.py @@ -43,7 +43,11 @@ MAX_EXTRACTED_BYTES = int( MAX_EXTRACTED_FILES = int(os.getenv("SHIM_MAX_EXTRACTED_FILES", "20000")) SESSION_COOKIE = "shim_session" ACTIVE_SITE_COOKIE = "shim_active_site" +MUTATING_METHODS = {"POST", "PUT", "PATCH", "DELETE"} SLUG_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$") +UUID_RE = re.compile( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" +) ARCHIVE_SUFFIXES = ( ".zip", ".tar", @@ -67,10 +71,11 @@ SHELL_TEMPLATE = """ + - {{ title }} - {{ app_name }} @@ -163,7 +198,7 @@ SETUP_BODY_TEMPLATE = """