Improper validation of URLs ('Cross-site Scripting') in Wagtail rich text fields
Description
Wagtail is a Django content management system. In affected versions of Wagtail, when saving the contents of a rich text field in the admin interface, Wagtail does not apply server-side checks to ensure that link URLs use a valid protocol. A malicious user with access to the admin interface could thus craft a POST request to publish content with javascript: URLs containing arbitrary code. The vulnerability is not exploitable by an ordinary site visitor without access to the Wagtail admin. See referenced GitHub advisory for additional details, including a workaround. Patched versions have been released as Wagtail 2.11.7 (for the LTS 2.11 branch) and Wagtail 2.12.4 (for the current 2.12 branch).
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
wagtailPyPI | < 2.11.7 | 2.11.7 |
wagtailPyPI | >= 2.12, < 2.12.4 | 2.12.4 |
Affected products
1Patches
25c7a60977cbaDisallow links with unrecognised protocols in contentstate
2 files changed · +55 −1
wagtail/admin/rich_text/converters/contentstate.py+2 −1 modified@@ -8,6 +8,7 @@ from wagtail.admin.rich_text.converters.html_to_contentstate import HtmlToContentStateHandler from wagtail.core.rich_text import features as feature_registry +from wagtail.core.whitelist import check_url def link_entity(props): @@ -21,7 +22,7 @@ def link_entity(props): link_props['linktype'] = 'page' link_props['id'] = id_ else: - link_props['href'] = props.get('url') + link_props['href'] = check_url(props.get('url')) return DOM.create_element('a', link_props, props['children'])
wagtail/admin/tests/test_contentstate.py+53 −0 modified@@ -825,3 +825,56 @@ def test_p_with_class(self): ], 'entityMap': {} }) + + +class TestContentStateToHtml(TestCase): + def test_external_link(self): + converter = ContentstateConverter(features=['link']) + contentstate_json = json.dumps({ + 'entityMap': { + '0': {'mutability': 'MUTABLE', 'type': 'LINK', 'data': {'url': 'http://wagtail.io'}} + }, + 'blocks': [ + { + 'inlineStyleRanges': [], 'text': 'an external link', 'depth': 0, 'type': 'unstyled', 'key': '00000', + 'entityRanges': [{'offset': 3, 'length': 8, 'key': 0}] + }, + ] + }) + + result = converter.to_database_format(contentstate_json) + self.assertEqual(result, '<p>an <a href="http://wagtail.io">external</a> link</p>') + + def test_local_link(self): + converter = ContentstateConverter(features=['link']) + contentstate_json = json.dumps({ + 'entityMap': { + '0': {'mutability': 'MUTABLE', 'type': 'LINK', 'data': {'url': '/some/local/path/'}} + }, + 'blocks': [ + { + 'inlineStyleRanges': [], 'text': 'an external link', 'depth': 0, 'type': 'unstyled', 'key': '00000', + 'entityRanges': [{'offset': 3, 'length': 8, 'key': 0}] + }, + ] + }) + + result = converter.to_database_format(contentstate_json) + self.assertEqual(result, '<p>an <a href="/some/local/path/">external</a> link</p>') + + def test_reject_javascript_link(self): + converter = ContentstateConverter(features=['link']) + contentstate_json = json.dumps({ + 'entityMap': { + '0': {'mutability': 'MUTABLE', 'type': 'LINK', 'data': {'url': "javascript:alert('oh no')"}} + }, + 'blocks': [ + { + 'inlineStyleRanges': [], 'text': 'an external link', 'depth': 0, 'type': 'unstyled', 'key': '00000', + 'entityRanges': [{'offset': 3, 'length': 8, 'key': 0}] + }, + ] + }) + + result = converter.to_database_format(contentstate_json) + self.assertEqual(result, '<p>an <a>external</a> link</p>')
915f6ed2bd7dDisallow links with unrecognised protocols in contentstate
2 files changed · +55 −1
wagtail/admin/rich_text/converters/contentstate.py+2 −1 modified@@ -8,6 +8,7 @@ from wagtail.admin.rich_text.converters.html_to_contentstate import HtmlToContentStateHandler from wagtail.core.rich_text import features as feature_registry +from wagtail.core.whitelist import check_url def link_entity(props): @@ -21,7 +22,7 @@ def link_entity(props): link_props['linktype'] = 'page' link_props['id'] = id_ else: - link_props['href'] = props.get('url') + link_props['href'] = check_url(props.get('url')) return DOM.create_element('a', link_props, props['children'])
wagtail/admin/tests/test_contentstate.py+53 −0 modified@@ -825,3 +825,56 @@ def test_p_with_class(self): ], 'entityMap': {} }) + + +class TestContentStateToHtml(TestCase): + def test_external_link(self): + converter = ContentstateConverter(features=['link']) + contentstate_json = json.dumps({ + 'entityMap': { + '0': {'mutability': 'MUTABLE', 'type': 'LINK', 'data': {'url': 'http://wagtail.io'}} + }, + 'blocks': [ + { + 'inlineStyleRanges': [], 'text': 'an external link', 'depth': 0, 'type': 'unstyled', 'key': '00000', + 'entityRanges': [{'offset': 3, 'length': 8, 'key': 0}] + }, + ] + }) + + result = converter.to_database_format(contentstate_json) + self.assertEqual(result, '<p>an <a href="http://wagtail.io">external</a> link</p>') + + def test_local_link(self): + converter = ContentstateConverter(features=['link']) + contentstate_json = json.dumps({ + 'entityMap': { + '0': {'mutability': 'MUTABLE', 'type': 'LINK', 'data': {'url': '/some/local/path/'}} + }, + 'blocks': [ + { + 'inlineStyleRanges': [], 'text': 'an external link', 'depth': 0, 'type': 'unstyled', 'key': '00000', + 'entityRanges': [{'offset': 3, 'length': 8, 'key': 0}] + }, + ] + }) + + result = converter.to_database_format(contentstate_json) + self.assertEqual(result, '<p>an <a href="/some/local/path/">external</a> link</p>') + + def test_reject_javascript_link(self): + converter = ContentstateConverter(features=['link']) + contentstate_json = json.dumps({ + 'entityMap': { + '0': {'mutability': 'MUTABLE', 'type': 'LINK', 'data': {'url': "javascript:alert('oh no')"}} + }, + 'blocks': [ + { + 'inlineStyleRanges': [], 'text': 'an external link', 'depth': 0, 'type': 'unstyled', 'key': '00000', + 'entityRanges': [{'offset': 3, 'length': 8, 'key': 0}] + }, + ] + }) + + result = converter.to_database_format(contentstate_json) + self.assertEqual(result, '<p>an <a>external</a> link</p>')
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-wq5h-f9p5-q7fxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-29434ghsaADVISORY
- github.com/pypa/advisory-database/tree/main/vulns/wagtail/PYSEC-2021-114.yamlghsaWEB
- github.com/wagtail/wagtail/commit/5c7a60977cba478f6a35390ba98cffc2bd41c8a4ghsaWEB
- github.com/wagtail/wagtail/commit/915f6ed2bd7d53154103cc4424a0f18695cdad6cghsaWEB
- github.com/wagtail/wagtail/compare/v2.11.6...v2.11.7ghsaWEB
- github.com/wagtail/wagtail/security/advisories/GHSA-wq5h-f9p5-q7fxghsax_refsource_CONFIRMWEB
- pypi.org/project/wagtailghsaWEB
- pypi.org/project/wagtail/mitrex_refsource_MISC
News mentions
0No linked articles in our index yet.