From 14aa6571de03743fe54e868589e5bf74eeaca566 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:17:27 +0000 Subject: [PATCH] WebHost: Fix: strict suuid converter (#6130) * WebHost: disallow '=' and whitespace in SUUID and append the correct number of '=' This makes Room IDs unambiguous and avoids breaking if b64decode ever becomes strict. * Tests, WebHost: add SUUID tests * Tests, WebHost: add edge cases to SUUID tests --- .github/pyright-config.json | 1 + WebHostLib/__init__.py | 4 +- test/webhost/test_suuid.py | 76 +++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 test/webhost/test_suuid.py diff --git a/.github/pyright-config.json b/.github/pyright-config.json index c5432dbf3c..7bd4698c74 100644 --- a/.github/pyright-config.json +++ b/.github/pyright-config.json @@ -19,6 +19,7 @@ "../test/programs/test_multi_server.py", "../test/utils/__init__.py", "../test/webhost/test_descriptions.py", + "../test/webhost/test_suuid.py", "../worlds/AutoSNIClient.py", "type_check.py" ], diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index f1ac7ad558..9459845a1d 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -71,7 +71,9 @@ CLI(app) def to_python(value: str) -> uuid.UUID: - return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '==')) + if "=" in value or any(c.isspace() for c in value): + raise ValueError("Invalid UUID format") + return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '=' * (-len(value) % 4))) def to_url(value: uuid.UUID) -> str: diff --git a/test/webhost/test_suuid.py b/test/webhost/test_suuid.py new file mode 100644 index 0000000000..a24b094df9 --- /dev/null +++ b/test/webhost/test_suuid.py @@ -0,0 +1,76 @@ +import math +from typing import Any, Callable +from typing_extensions import override +from uuid import uuid4 + +from werkzeug.routing import BaseConverter + +from . import TestBase + + +class TestSUUID(TestBase): + converter: BaseConverter + filter: Callable[[Any], str] + + @override + def setUp(self) -> None: + from werkzeug.routing import Map + + super().setUp() + self.converter = self.app.url_map.converters["suuid"](Map()) + self.filter = self.app.jinja_env.filters["suuid"] # type: ignore # defines how we use it, not what it can be + + def test_is_reversible(self) -> None: + u = uuid4() + self.assertEqual(u, self.converter.to_python(self.converter.to_url(u))) + s = "A" * 22 # uuid with all zeros + self.assertEqual(s, self.converter.to_url(self.converter.to_python(s))) + + def test_uuid_length(self) -> None: + with self.assertRaises(ValueError): + self.converter.to_python("AAAA") + + def test_padding(self) -> None: + self.converter.to_python("A" * 22) # check that the correct value works + with self.assertRaises(ValueError): + self.converter.to_python("A" * 22 + "==") # converter should not allow padding + + def test_empty(self) -> None: + with self.assertRaises(ValueError): + self.converter.to_python("") + + def test_stray_equal_signs(self) -> None: + self.converter.to_python("A" * 22) # check that the correct value works + with self.assertRaises(ValueError): + self.converter.to_python("A" * 22 + "==" + "AA") # the "==AA" should not be ignored, but error out + with self.assertRaises(ValueError): + self.converter.to_python("A" * 20 + "==" + "AA") # the final "A"s should not be appended to the first "A"s + + def test_stray_whitespace(self) -> None: + s = "A" * 22 + self.converter.to_python(s) # check that the correct value works + for char in " \t\r\n\v": + for pos in (0, 11, 22): + with self.subTest(char=char, pos=pos): + s_with_whitespace = s[0:pos] + char * 4 + s[pos:] # insert 4 to make padding correct + # check that the constructed s_with_whitespace is correct + self.assertEqual(len(s_with_whitespace), len(s) + 4) + self.assertEqual(s_with_whitespace[pos], char) + # s_with_whitespace should be invalid as SUUID + with self.assertRaises(ValueError): + self.converter.to_python(s_with_whitespace) + + def test_filter_returns_valid_string(self) -> None: + u = uuid4() + s = self.filter(u) + self.assertIsInstance(s, str) + self.assertNotIn("=", s) + self.assertEqual(len(s), math.ceil(len(u.bytes) * 4 / 3)) + + def test_filter_is_same_as_converter(self) -> None: + u = uuid4() + self.assertEqual(self.filter(u), self.converter.to_url(u)) + + def test_filter_bad_type(self) -> None: + with self.assertRaises(Exception): # currently the type is not checked directly, so any exception is valid + self.filter(None)