summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNatalia <124304+nessita@users.noreply.github.com>2026-01-29 22:52:41 -0300
committerNatalia <124304+nessita@users.noreply.github.com>2026-03-03 09:10:53 -0300
commitb1444d9acf43db9de96e0da2b4737ad56af0eb76 (patch)
treee784213e3c584878234d5d9afabbba6a17abf552
parent1b22d53bf67943cd193bbd6e327d955c19d2f5d2 (diff)
[6.0.x] Fixed CVE-2026-25673 -- Simplified URLField scheme detection.
This simplicaftion mitigates a potential DoS in URLField on Windows. The usage of `urlsplit()` in `URLField.to_python()` was replaced with `str.partition(":")` for URL scheme detection. On Windows, `urlsplit()` performs Unicode normalization which is slow for certain characters, making `URLField` vulnerable to DoS via specially crafted POST payloads. Thanks Seokchan Yoon for the report, and Jake Howard and Shai Berger for the review. Refs #36923. Co-authored-by: Jacob Walls <jacobtylerwalls@gmail.com> Backport of 951ffb3832cd83ba672c1e3deae2bda128eb9cca from main.
-rw-r--r--django/forms/fields.py42
-rw-r--r--docs/releases/4.2.29.txt22
-rw-r--r--docs/releases/5.2.12.txt22
-rw-r--r--docs/releases/6.0.3.txt22
-rw-r--r--tests/forms_tests/field_tests/test_urlfield.py85
5 files changed, 165 insertions, 28 deletions
diff --git a/django/forms/fields.py b/django/forms/fields.py
index a5eee6831c..6408be2f06 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -12,7 +12,6 @@ import re
import uuid
from decimal import Decimal, DecimalException
from io import BytesIO
-from urllib.parse import urlsplit, urlunsplit
from django.core import validators
from django.core.exceptions import ValidationError
@@ -780,33 +779,24 @@ class URLField(CharField):
super().__init__(strip=True, **kwargs)
def to_python(self, value):
- def split_url(url):
- """
- Return a list of url parts via urlsplit(), or raise
- ValidationError for some malformed URLs.
- """
- try:
- return list(urlsplit(url))
- except ValueError:
- # urlsplit can raise a ValueError with some
- # misformatted URLs.
- raise ValidationError(self.error_messages["invalid"], code="invalid")
-
value = super().to_python(value)
if value:
- url_fields = split_url(value)
- if not url_fields[0]:
- # If no URL scheme given, add a scheme.
- url_fields[0] = self.assume_scheme
- if not url_fields[1]:
- # Assume that if no domain is provided, that the path segment
- # contains the domain.
- url_fields[1] = url_fields[2]
- url_fields[2] = ""
- # Rebuild the url_fields list, since the domain segment may now
- # contain the path too.
- url_fields = split_url(urlunsplit(url_fields))
- value = urlunsplit(url_fields)
+ # Detect scheme via partition to avoid calling urlsplit() on
+ # potentially large or slow-to-normalize inputs.
+ scheme, sep, _ = value.partition(":")
+ if (
+ not sep
+ or not scheme
+ or not scheme[0].isascii()
+ or not scheme[0].isalpha()
+ or "/" in scheme
+ ):
+ # No valid scheme found -- prepend the assumed scheme. Handle
+ # scheme-relative URLs ("//example.com") separately.
+ if value.startswith("//"):
+ value = self.assume_scheme + ":" + value
+ else:
+ value = self.assume_scheme + "://" + value
return value
diff --git a/docs/releases/4.2.29.txt b/docs/releases/4.2.29.txt
index a3f3787cd6..b780264929 100644
--- a/docs/releases/4.2.29.txt
+++ b/docs/releases/4.2.29.txt
@@ -6,3 +6,25 @@ Django 4.2.29 release notes
Django 4.2.29 fixes a security issue with severity "moderate" and a security
issue with severity "low" in 4.2.28.
+
+CVE-2026-25673: Potential denial-of-service vulnerability in ``URLField`` via Unicode normalization on Windows
+==============================================================================================================
+
+The :class:`~django.forms.URLField` form field's ``to_python()`` method used
+:func:`~urllib.parse.urlsplit` to determine whether to prepend a URL scheme to
+the submitted value. On Windows, ``urlsplit()`` performs
+:func:`NFKC normalization <python:unicodedata.normalize>`, which can be
+disproportionately slow for large inputs containing certain characters.
+
+``URLField.to_python()`` now uses a simplified scheme detection, avoiding
+Unicode normalization entirely and deferring URL validation to the appropriate
+layers. As a result, while leading and trailing whitespace is still stripped by
+default, characters such as newlines, tabs, and other control characters within
+the value are no longer handled by ``URLField.to_python()``. When using the
+default :class:`~django.core.validators.URLValidator`, these values will
+continue to raise :exc:`~django.core.exceptions.ValidationError` during
+validation, but if you rely on custom validators, ensure they do not depend on
+the previous behavior of ``URLField.to_python()``.
+
+This issue has severity "moderate" according to the :ref:`Django security
+policy <security-disclosure>`.
diff --git a/docs/releases/5.2.12.txt b/docs/releases/5.2.12.txt
index 9cbbf3836a..be2c7bc807 100644
--- a/docs/releases/5.2.12.txt
+++ b/docs/releases/5.2.12.txt
@@ -8,6 +8,28 @@ Django 5.2.12 fixes a security issue with severity "moderate" and a security
issue with severity "low" in 5.2.11. It also fixes one bug related to support
for Python 3.14.
+CVE-2026-25673: Potential denial-of-service vulnerability in ``URLField`` via Unicode normalization on Windows
+==============================================================================================================
+
+The :class:`~django.forms.URLField` form field's ``to_python()`` method used
+:func:`~urllib.parse.urlsplit` to determine whether to prepend a URL scheme to
+the submitted value. On Windows, ``urlsplit()`` performs
+:func:`NFKC normalization <python:unicodedata.normalize>`, which can be
+disproportionately slow for large inputs containing certain characters.
+
+``URLField.to_python()`` now uses a simplified scheme detection, avoiding
+Unicode normalization entirely and deferring URL validation to the appropriate
+layers. As a result, while leading and trailing whitespace is still stripped by
+default, characters such as newlines, tabs, and other control characters within
+the value are no longer handled by ``URLField.to_python()``. When using the
+default :class:`~django.core.validators.URLValidator`, these values will
+continue to raise :exc:`~django.core.exceptions.ValidationError` during
+validation, but if you rely on custom validators, ensure they do not depend on
+the previous behavior of ``URLField.to_python()``.
+
+This issue has severity "moderate" according to the :ref:`Django security
+policy <security-disclosure>`.
+
Bugfixes
========
diff --git a/docs/releases/6.0.3.txt b/docs/releases/6.0.3.txt
index 8777b1ef21..6750385c1e 100644
--- a/docs/releases/6.0.3.txt
+++ b/docs/releases/6.0.3.txt
@@ -7,6 +7,28 @@ Django 6.0.3 release notes
Django 6.0.3 fixes a security issue with severity "moderate", a security issue
with severity "low", and several bugs in 6.0.2.
+CVE-2026-25673: Potential denial-of-service vulnerability in ``URLField`` via Unicode normalization on Windows
+==============================================================================================================
+
+The :class:`~django.forms.URLField` form field's ``to_python()`` method used
+:func:`~urllib.parse.urlsplit` to determine whether to prepend a URL scheme to
+the submitted value. On Windows, ``urlsplit()`` performs
+:func:`NFKC normalization <python:unicodedata.normalize>`, which can be
+disproportionately slow for large inputs containing certain characters.
+
+``URLField.to_python()`` now uses a simplified scheme detection, avoiding
+Unicode normalization entirely and deferring URL validation to the appropriate
+layers. As a result, while leading and trailing whitespace is still stripped by
+default, characters such as newlines, tabs, and other control characters within
+the value are no longer handled by ``URLField.to_python()``. When using the
+default :class:`~django.core.validators.URLValidator`, these values will
+continue to raise :exc:`~django.core.exceptions.ValidationError` during
+validation, but if you rely on custom validators, ensure they do not depend on
+the previous behavior of ``URLField.to_python()``.
+
+This issue has severity "moderate" according to the :ref:`Django security
+policy <security-disclosure>`.
+
Bugfixes
========
diff --git a/tests/forms_tests/field_tests/test_urlfield.py b/tests/forms_tests/field_tests/test_urlfield.py
index f7d318fdc9..87d3489687 100644
--- a/tests/forms_tests/field_tests/test_urlfield.py
+++ b/tests/forms_tests/field_tests/test_urlfield.py
@@ -1,4 +1,5 @@
from django.core.exceptions import ValidationError
+from django.core.validators import URLValidator
from django.forms import URLField
from django.test import SimpleTestCase
@@ -72,6 +73,16 @@ class URLFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
# IPv6.
("http://[12:34::3a53]/", "http://[12:34::3a53]/"),
("http://[a34:9238::]:8080/", "http://[a34:9238::]:8080/"),
+ # IPv6 without scheme.
+ ("[12:34::3a53]/", "https://[12:34::3a53]/"),
+ # IDN domain without scheme but with port.
+ ("ñandú.es:8080/", "https://ñandú.es:8080/"),
+ # Scheme-relative.
+ ("//example.com", "https://example.com"),
+ ("//example.com/path", "https://example.com/path"),
+ # Whitespace stripped.
+ ("\t\n//example.com \n\t\n", "https://example.com"),
+ ("\t\nhttp://example.com \n\t\n", "http://example.com"),
]
for url, expected in tests:
with self.subTest(url=url):
@@ -102,10 +113,19 @@ class URLFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
# even on domains that don't fail the domain label length check in
# the regex.
"http://%s" % ("X" * 200,),
- # urlsplit() raises ValueError.
+ # Scheme prepend yields a structurally invalid URL.
"////]@N.AN",
- # Empty hostname.
+ # Scheme prepend yields an empty hostname.
"#@A.bO",
+ # Known problematic unicode chars.
+ "http://" + "¾" * 200,
+ # Non-ASCII character before the first colon.
+ "¾:example.com",
+ # ASCII digit before the first colon.
+ "1http://example.com",
+ # Empty scheme.
+ "://example.com",
+ ":example.com",
]
msg = "'Enter a valid URL.'"
for value in tests:
@@ -143,3 +163,64 @@ class URLFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
self.assertEqual(f.clean("example.com"), "http://example.com")
f = URLField(assume_scheme="https")
self.assertEqual(f.clean("example.com"), "https://example.com")
+
+ def test_urlfield_assume_scheme_when_colons(self):
+ f = URLField()
+ tests = [
+ # Port number.
+ ("http://example.com:8080/", "http://example.com:8080/"),
+ ("https://example.com:443/path", "https://example.com:443/path"),
+ # Userinfo with password.
+ ("http://user:pass@example.com", "http://user:pass@example.com"),
+ (
+ "http://user:pass@example.com:8080/",
+ "http://user:pass@example.com:8080/",
+ ),
+ # Colon in path segment.
+ ("http://example.com/path:segment", "http://example.com/path:segment"),
+ ("http://example.com/a:b/c:d", "http://example.com/a:b/c:d"),
+ # Colon in query string.
+ ("http://example.com/?key=val:ue", "http://example.com/?key=val:ue"),
+ # Colon in fragment.
+ ("http://example.com/#section:1", "http://example.com/#section:1"),
+ # IPv6 -- multiple colons in host.
+ ("http://[::1]/", "http://[::1]/"),
+ ("http://[2001:db8::1]/", "http://[2001:db8::1]/"),
+ ("http://[2001:db8::1]:8080/", "http://[2001:db8::1]:8080/"),
+ # Colons across multiple components.
+ (
+ "http://user:pass@example.com:8080/path:x?q=a:b#id:1",
+ "http://user:pass@example.com:8080/path:x?q=a:b#id:1",
+ ),
+ # FTP with port and userinfo.
+ (
+ "ftp://user:pass@ftp.example.com:21/file",
+ "ftp://user:pass@ftp.example.com:21/file",
+ ),
+ (
+ "ftps://user:pass@ftp.example.com:990/",
+ "ftps://user:pass@ftp.example.com:990/",
+ ),
+ # Scheme-relative URLs, starts with "//".
+ ("//example.com:8080/path", "https://example.com:8080/path"),
+ ("//user:pass@example.com/", "https://user:pass@example.com/"),
+ ]
+ for value, expected in tests:
+ with self.subTest(value=value):
+ self.assertEqual(f.clean(value), expected)
+
+ def test_custom_validator_longer_max_length(self):
+
+ class CustomLongURLValidator(URLValidator):
+ max_length = 4096
+
+ class CustomURLField(URLField):
+ default_validators = [CustomLongURLValidator()]
+
+ field = CustomURLField()
+ # A URL with 4096 chars is valid given the custom validator.
+ prefix = "https://example.com/"
+ url = prefix + "a" * (4096 - len(prefix))
+ self.assertEqual(len(url), 4096)
+ # No ValidationError is raised.
+ field.clean(url)