commit b7ed6065f322c759f90ed83e7ddef569cb4d13f8
Author: Carlton Gibson <carlton.gibson@noumenal.es>
Date:   Mon May 27 12:10:02 2019 +0200

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

diff --git a/django/contrib/admin/templates/admin/widgets/url.html b/django/contrib/admin/templates/admin/widgets/url.html
index 554a9343fe..629a740664 100644
--- a/django/contrib/admin/templates/admin/widgets/url.html
+++ b/django/contrib/admin/templates/admin/widgets/url.html
@@ -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 %}
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index 62da6265b6..209e0289e3 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -7,6 +7,7 @@ import copy
 
 from django import forms
 from django.core.exceptions import ValidationError
+from django.core.validators import URLValidator
 from django.db.models.deletion import CASCADE
 from django.urls import reverse
 from django.urls.exceptions import NoReverseMatch
@@ -339,17 +340,24 @@ class AdminEmailInputWidget(forms.EmailInput):
 class AdminURLFieldWidget(forms.URLInput):
     template_name = 'admin/widgets/url.html'
 
-    def __init__(self, attrs=None):
+    def __init__(self, attrs=None, validator_class=URLValidator):
         final_attrs = {'class': 'vURLField'}
         if attrs is not None:
             final_attrs.update(attrs)
         super(AdminURLFieldWidget, self).__init__(attrs=final_attrs)
+        self.validator = validator_class()
 
     def get_context(self, name, value, attrs):
+        try:
+            self.validator(value if value else '')
+            url_valid = True
+        except ValidationError:
+            url_valid = False
         context = super(AdminURLFieldWidget, self).get_context(name, value, attrs)
         context['current_label'] = _('Currently:')
         context['change_label'] = _('Change:')
         context['widget']['href'] = smart_urlquote(context['widget']['value']) if value else ''
+        context['url_valid'] = url_valid
         return context
 
 
diff --git a/docs/releases/1.11.21.txt b/docs/releases/1.11.21.txt
new file mode 100644
index 0000000000..3da7a78612
--- /dev/null
+++ b/docs/releases/1.11.21.txt
@@ -0,0 +1,21 @@
+============================
+Django 1.11.21 release notes
+============================
+
+*June 3, 2019*
+
+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`.
diff --git a/docs/releases/index.txt b/docs/releases/index.txt
index e2da7c3de6..c2e8503102 100644
--- a/docs/releases/index.txt
+++ b/docs/releases/index.txt
@@ -26,6 +26,7 @@ versions of the documentation contain the release notes for any later releases.
 .. toctree::
    :maxdepth: 1
 
+   1.11.21
    1.11.20
    1.11.19
    1.11.18
diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py
index 4d78d5fd5b..96fbfc1252 100644
--- a/tests/admin_widgets/tests.py
+++ b/tests/admin_widgets/tests.py
@@ -336,6 +336,12 @@ class AdminSplitDateTimeWidgetTest(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!")']:
+            self.assertFalse(w.get_context('name', invalid, {})['url_valid'])
+        self.assertTrue(w.get_context('name', 'http://example.com', {})['url_valid'])
+
     def test_render(self):
         w = widgets.AdminURLFieldWidget()
         self.assertHTMLEqual(
@@ -369,31 +375,31 @@ class AdminURLWidgetTest(SimpleTestCase):
         VALUE_RE = re.compile('value="([^"]+)"')
         TEXT_RE = re.compile('<a[^>]+>([^>]+)</a>')
         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(
             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(
             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(
             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(
             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(
             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(
             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>"')
         self.assertEqual(
