Compare commits

...

1 Commits

Author SHA1 Message Date
lebaudantoine e5c9dc1fa8 🦺(backend) optionally validate room names using a regex
In production, room names follow a specific pattern that was only
enforced on the frontend. This caused minor bounties during the bug
bounty.

Move validation to the API to ensure the field is always validated
server-side.

Following OWASP recommendations for DRF, treat invalid room names
as suspicious input, since such requests cannot originate from the
official frontend client.
2026-03-11 16:14:07 +01:00
4 changed files with 73 additions and 2 deletions
+3 -2
View File
@@ -19,12 +19,13 @@ and this project adheres to
- ♿️(frontend) sync html lang attribute with i18n for screen readers #1111
- ♿️(frontend) improve MoreLink a11y and UX on home page #1112
- ♿(frontend) improve chat toast a11y for screen readers #1109
- ♿(frontend) improve ui and qria labels for help article links #1108
- ♿(frontend) improve ui and aria labels for help article links #1108
- 🌐(frontend) improve German translation #1125
- 🦺(backend) optionally validate room names using a regex #1132
### Fixed
- 🐛(frontend) fix hand icon and queue position aligment and position #1119
- 🐛(frontend) fix hand icon and queue position alignment and position #1119
## [1.10.0] - 2026-03-05
+16
View File
@@ -2,6 +2,7 @@
# pylint: disable=abstract-method,no-name-in-module
import logging
import re
from os.path import splitext
from typing import Literal
from urllib.parse import quote
@@ -131,6 +132,21 @@ class RoomSerializer(serializers.ModelSerializer):
fields = ["id", "name", "slug", "configuration", "access_level", "pin_code"]
read_only_fields = ["id", "slug", "pin_code"]
def validate_name(self, value):
"""Validate the name matches the optionally configured ROOM_NAME_REGEX"""
if not value:
raise serializers.ValidationError("Name cannot be empty.")
room_name_regex = settings.ROOM_NAME_REGEX
if room_name_regex is None:
return value
if not re.fullmatch(room_name_regex, value):
raise SuspiciousOperation("Name does not match the expected format.")
return value
def to_representation(self, instance):
"""
Add users only for administrator users.
@@ -3,7 +3,10 @@ Test rooms API endpoints in the Meet core app: create.
"""
# pylint: disable=redefined-outer-name,unused-argument
from unittest import mock
from django.core.cache import cache
from django.core.exceptions import SuspiciousOperation
import pytest
from rest_framework.test import APIClient
@@ -109,3 +112,47 @@ def test_api_rooms_create_authenticated_existing_slug():
assert response.status_code == 400
assert response.json() == {"slug": ["Room with this Slug already exists."]}
@mock.patch("core.api.serializers.SuspiciousOperation", side_effect=SuspiciousOperation)
def test_api_rooms_create_invalid_regex(mock_suspicious, settings):
"""A room can not be created when its name doesn't match the configured pattern."""
settings.ROOM_NAME_REGEX = r"[a-z]{3}-[a-z]{4}-[a-z]{3}"
user = UserFactory()
client = APIClient()
client.force_login(user)
response = client.post(
"/api/v1.0/rooms/",
{
"name": "my room",
},
)
assert response.status_code == 400
assert Room.objects.exists() is False
mock_suspicious.assert_called_once_with("Name does not match the expected format.")
def test_api_rooms_create_valid_regex(settings):
"""A room can be created when its name matches the configured pattern."""
settings.ROOM_NAME_REGEX = r"[a-z]{3}-[a-z]{4}-[a-z]{3}"
user = UserFactory()
client = APIClient()
client.force_login(user)
response = client.post(
"/api/v1.0/rooms/",
{
"name": "foo-barn-baz",
},
)
assert response.status_code == 201
assert Room.objects.count() == 1
assert Room.objects.filter(name="foo-barn-baz").exists()
+7
View File
@@ -752,6 +752,13 @@ class Base(Configuration):
environ_prefix=None,
)
# Room validation
ROOM_NAME_REGEX = values.RegexValue(
None,
environ_name="ROOM_NAME_REGEX",
environ_prefix=None,
)
# Calendar integrations
ROOM_CREATION_CALLBACK_CACHE_TIMEOUT = values.PositiveIntegerValue(
600, # 10 minutes