Skip to content

Commit d6814fd

Browse files
authored
Security: avoid requests with NULL characters (0x00) on GET (#9350)
1 parent 0a7df63 commit d6814fd

File tree

3 files changed

+41
-3
lines changed

3 files changed

+41
-3
lines changed

readthedocs/core/middleware.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import structlog
21
import time
32

3+
import structlog
44
from django.conf import settings
55
from django.contrib.sessions.backends.base import SessionBase, UpdateError
66
from django.contrib.sessions.middleware import SessionMiddleware
@@ -11,7 +11,6 @@
1111
)
1212
from django.utils.cache import patch_vary_headers
1313
from django.utils.http import http_date
14-
from django.utils.translation import gettext_lazy as _
1514

1615
log = structlog.get_logger(__name__)
1716

@@ -192,3 +191,27 @@ def __call__(self, request):
192191
response = self.get_response(request)
193192
response['Referrer-Policy'] = settings.SECURE_REFERRER_POLICY
194193
return response
194+
195+
196+
class NullCharactersMiddleware:
197+
198+
"""
199+
Block all requests that contains NULL characters (0x00) on their GET attributes.
200+
201+
Requests containing NULL characters make our code to break. In particular,
202+
when trying to save the content containing a NULL character into the
203+
database, producing a 500 and creating an event in Sentry.
204+
205+
NULL characters are also used as an explotation technique, known as "Null Byte Injection".
206+
"""
207+
208+
def __init__(self, get_response):
209+
self.get_response = get_response
210+
211+
def __call__(self, request):
212+
for key, value in request.GET.items():
213+
if "\x00" in value:
214+
raise SuspiciousOperation(
215+
f"There are NULL (0x00) characters in at least one of the parameters ({key}) passed to the request." # noqa
216+
)
217+
return self.get_response(request)

readthedocs/rtd_tests/tests/test_middleware.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
from corsheaders.middleware import CorsMiddleware
44
from django.conf import settings
5+
from django.core.exceptions import SuspiciousOperation
56
from django.http import HttpResponse
67
from django.test import TestCase, override_settings
78
from django.test.client import RequestFactory
89
from django_dynamic_fixture import get
910

1011
from readthedocs.builds.constants import LATEST
11-
from readthedocs.core.middleware import ReadTheDocsSessionMiddleware
12+
from readthedocs.core.middleware import (
13+
NullCharactersMiddleware,
14+
ReadTheDocsSessionMiddleware,
15+
)
1216
from readthedocs.projects.constants import PRIVATE, PUBLIC
1317
from readthedocs.projects.models import Domain, Project, ProjectRelationship
1418
from readthedocs.rtd_tests.utils import create_user
@@ -259,3 +263,13 @@ def test_main_cookie_samesite_lax(self):
259263

260264
self.assertEqual(response.cookies[settings.SESSION_COOKIE_NAME]['samesite'], 'Lax')
261265
self.assertTrue(self.test_main_cookie_samesite_none not in response.cookies)
266+
267+
268+
class TestNullCharactersMiddleware(TestCase):
269+
def setUp(self):
270+
self.factory = RequestFactory()
271+
self.middleware = NullCharactersMiddleware(None)
272+
273+
def test_request_with_null_chars(self):
274+
request = self.factory.get("/?language=en\x00es&project_slug=myproject")
275+
self.assertRaises(SuspiciousOperation, lambda: self.middleware(request))

readthedocs/settings/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ def USE_PROMOS(self): # noqa
236236
return 'readthedocsext.donate' in self.INSTALLED_APPS
237237

238238
MIDDLEWARE = (
239+
'readthedocs.core.middleware.NullCharactersMiddleware',
239240
'readthedocs.core.middleware.ReadTheDocsSessionMiddleware',
240241
'django.middleware.locale.LocaleMiddleware',
241242
'django.middleware.common.CommonMiddleware',

0 commit comments

Comments
 (0)