Fixed CVE-2019-12308 -- Made AdminURLFieldWidget validate URL before rendering clickable link.

This commit is contained in:
Carlton Gibson 2019-05-23 12:06:34 +02:00
parent 98c0fe19ee
commit deeba6d920
6 changed files with 68 additions and 11 deletions

View File

@ -1 +1 @@
{% if widget.value %}<p class="url">{{ current_label }} <a href="{{ widget.href }}">{{ widget.value }}</a><br>{{ change_label }} {% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.value %}</p>{% endif %} {% if url_valid %}<p class="url">{{ current_label }} <a href="{{ widget.href }}">{{ widget.value }}</a><br>{{ change_label }} {% endif %}{% include "django/forms/widgets/input.html" %}{% if url_valid %}</p>{% endif %}

View File

@ -7,6 +7,7 @@ import json
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.db.models.deletion import CASCADE from django.db.models.deletion import CASCADE
from django.urls import reverse from django.urls import reverse
from django.urls.exceptions import NoReverseMatch from django.urls.exceptions import NoReverseMatch
@ -330,14 +331,21 @@ class AdminEmailInputWidget(forms.EmailInput):
class AdminURLFieldWidget(forms.URLInput): class AdminURLFieldWidget(forms.URLInput):
template_name = 'admin/widgets/url.html' template_name = 'admin/widgets/url.html'
def __init__(self, attrs=None): def __init__(self, attrs=None, validator_class=URLValidator):
super().__init__(attrs={'class': 'vURLField', **(attrs or {})}) super().__init__(attrs={'class': 'vURLField', **(attrs or {})})
self.validator = validator_class()
def get_context(self, name, value, attrs): def get_context(self, name, value, attrs):
try:
self.validator(value if value else '')
url_valid = True
except ValidationError:
url_valid = False
context = super().get_context(name, value, attrs) context = super().get_context(name, value, attrs)
context['current_label'] = _('Currently:') context['current_label'] = _('Currently:')
context['change_label'] = _('Change:') context['change_label'] = _('Change:')
context['widget']['href'] = smart_urlquote(context['widget']['value']) if value else '' context['widget']['href'] = smart_urlquote(context['widget']['value']) if value else ''
context['url_valid'] = url_valid
return context return context

View File

@ -4,4 +4,18 @@ Django 1.11.21 release notes
*June 3, 2019* *June 3, 2019*
Django 1.11.21 fixes security issues in 1.11.20. Django 1.11.21 fixes a security issue in 1.11.20.
CVE-2019-12308: AdminURLFieldWidget XSS
---------------------------------------
The clickable "Current URL" link generated by ``AdminURLFieldWidget`` displayed
the provided value without validating it as a safe URL. Thus, an unvalidated
value stored in the database, or a value provided as a URL query parameter
payload, could result in an clickable JavaScript link.
``AdminURLFieldWidget`` now validates the provided value using
:class:`~django.core.validators.URLValidator` before displaying the clickable
link. You may customise the validator by passing a ``validator_class`` kwarg to
``AdminURLFieldWidget.__init__()``, e.g. when using
:attr:`~django.contrib.admin.ModelAdmin.formfield_overrides`.

View File

@ -5,3 +5,17 @@ Django 2.1.9 release notes
*June 3, 2019* *June 3, 2019*
Django 2.1.9 fixes security issues in 2.1.8. Django 2.1.9 fixes security issues in 2.1.8.
CVE-2019-12308: AdminURLFieldWidget XSS
---------------------------------------
The clickable "Current URL" link generated by ``AdminURLFieldWidget`` displayed
the provided value without validating it as a safe URL. Thus, an unvalidated
value stored in the database, or a value provided as a URL query parameter
payload, could result in an clickable JavaScript link.
``AdminURLFieldWidget`` now validates the provided value using
:class:`~django.core.validators.URLValidator` before displaying the clickable
link. You may customise the validator by passing a ``validator_class`` kwarg to
``AdminURLFieldWidget.__init__()``, e.g. when using
:attr:`~django.contrib.admin.ModelAdmin.formfield_overrides`.

View File

@ -6,6 +6,20 @@ Django 2.2.2 release notes
Django 2.2.2 fixes security issues and several bugs in 2.2.1. Django 2.2.2 fixes security issues and several bugs in 2.2.1.
CVE-2019-12308: AdminURLFieldWidget XSS
---------------------------------------
The clickable "Current URL" link generated by ``AdminURLFieldWidget`` displayed
the provided value without validating it as a safe URL. Thus, an unvalidated
value stored in the database, or a value provided as a URL query parameter
payload, could result in an clickable JavaScript link.
``AdminURLFieldWidget`` now validates the provided value using
:class:`~django.core.validators.URLValidator` before displaying the clickable
link. You may customise the validator by passing a ``validator_class`` kwarg to
``AdminURLFieldWidget.__init__()``, e.g. when using
:attr:`~django.contrib.admin.ModelAdmin.formfield_overrides`.
Bugfixes Bugfixes
======== ========

View File

@ -333,6 +333,13 @@ class AdminSplitDateTimeWidgetTest(SimpleTestCase):
class AdminURLWidgetTest(SimpleTestCase): class AdminURLWidgetTest(SimpleTestCase):
def test_get_context_validates_url(self):
w = widgets.AdminURLFieldWidget()
for invalid in ['', '/not/a/full/url/', 'javascript:alert("Danger XSS!")']:
with self.subTest(url=invalid):
self.assertFalse(w.get_context('name', invalid, {})['url_valid'])
self.assertTrue(w.get_context('name', 'http://example.com', {})['url_valid'])
def test_render(self): def test_render(self):
w = widgets.AdminURLFieldWidget() w = widgets.AdminURLFieldWidget()
self.assertHTMLEqual( self.assertHTMLEqual(
@ -366,31 +373,31 @@ class AdminURLWidgetTest(SimpleTestCase):
VALUE_RE = re.compile('value="([^"]+)"') VALUE_RE = re.compile('value="([^"]+)"')
TEXT_RE = re.compile('<a[^>]+>([^>]+)</a>') TEXT_RE = re.compile('<a[^>]+>([^>]+)</a>')
w = widgets.AdminURLFieldWidget() w = widgets.AdminURLFieldWidget()
output = w.render('test', 'http://example.com/<sometag>some text</sometag>') output = w.render('test', 'http://example.com/<sometag>some-text</sometag>')
self.assertEqual( self.assertEqual(
HREF_RE.search(output).groups()[0], HREF_RE.search(output).groups()[0],
'http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E', 'http://example.com/%3Csometag%3Esome-text%3C/sometag%3E',
) )
self.assertEqual( self.assertEqual(
TEXT_RE.search(output).groups()[0], TEXT_RE.search(output).groups()[0],
'http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;', 'http://example.com/&lt;sometag&gt;some-text&lt;/sometag&gt;',
) )
self.assertEqual( self.assertEqual(
VALUE_RE.search(output).groups()[0], VALUE_RE.search(output).groups()[0],
'http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;', 'http://example.com/&lt;sometag&gt;some-text&lt;/sometag&gt;',
) )
output = w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>') output = w.render('test', 'http://example-äüö.com/<sometag>some-text</sometag>')
self.assertEqual( self.assertEqual(
HREF_RE.search(output).groups()[0], HREF_RE.search(output).groups()[0],
'http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E', 'http://xn--example--7za4pnc.com/%3Csometag%3Esome-text%3C/sometag%3E',
) )
self.assertEqual( self.assertEqual(
TEXT_RE.search(output).groups()[0], TEXT_RE.search(output).groups()[0],
'http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;', 'http://example-äüö.com/&lt;sometag&gt;some-text&lt;/sometag&gt;',
) )
self.assertEqual( self.assertEqual(
VALUE_RE.search(output).groups()[0], VALUE_RE.search(output).groups()[0],
'http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;', 'http://example-äüö.com/&lt;sometag&gt;some-text&lt;/sometag&gt;',
) )
output = w.render('test', 'http://www.example.com/%C3%A4"><script>alert("XSS!")</script>"') output = w.render('test', 'http://www.example.com/%C3%A4"><script>alert("XSS!")</script>"')
self.assertEqual( self.assertEqual(