Closes #22652: Disable autoescaping for Config Templates (#22653)

Force autoescape=False in ConfigTemplate.get_environment_params() after
merging user-supplied environment parameters. Config templates produce
plain-text network configurations and scripts, so HTML autoescaping is
not applicable.

Keep the override out of the shared render_jinja2() helper so export
templates can continue to use autoescape=True for HTML output. Add
regression coverage for both behaviors.
This commit is contained in:
bctiemann 2026-07-11 12:37:52 -04:00 committed by GitHub
parent f250586b4c
commit ca7caecac5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 34 additions and 3 deletions

View File

@ -316,6 +316,16 @@ class ConfigTemplate(
self.template_code = self.data_file.data_as_string
sync_data.alters_data = True
def get_environment_params(self):
"""
Config templates render plain text (network configs, scripts), not HTML. Force
autoescape off so environment_params cannot enable it and create a latent XSS sink
if output is ever rendered in an HTML context.
"""
params = super().get_environment_params()
params['autoescape'] = False
return params
def format_render_error(self, exc):
"""
Return a formatted error string for a rendering exception. When debug is enabled, the full

View File

@ -1310,6 +1310,24 @@ class RenderTemplateMixinRenderTestCase(TestCase):
self.assertNotEqual(plain.render(ctx), trimmed.render(ctx))
self.assertEqual(trimmed.render(ctx).strip(), 'VALUE')
def test_configtemplate_autoescape_always_disabled(self):
"""
ConfigTemplate renders plain text (network configs, scripts); autoescape must stay off
even if environment_params explicitly requests it (#22652).
"""
t = ConfigTemplate(name='autoescape', template_code='{{ value }}', environment_params={'autoescape': True})
self.assertEqual(t.render({'value': '<script>'}), '<script>')
def test_exporttemplate_autoescape_is_configurable(self):
"""
Unlike ConfigTemplate, ExportTemplate output may legitimately be HTML, so an explicit
autoescape=True in environment_params must be honored rather than forced off.
"""
et = ExportTemplate(
name='autoescape', template_code='{{ value }}', environment_params={'autoescape': True}
)
self.assertEqual(et.render({'value': '<script>'}), '&lt;script&gt;')
def test_environment_params_undefined_path_import(self):
# Default Undefined renders nothing for a missing variable.
default = ConfigTemplate(name='default', template_code='{{ missing }}')
@ -1339,8 +1357,9 @@ class RenderTemplateMixinRenderTestCase(TestCase):
def test_get_environment_params_handles_none(self):
# The environment_params field may be cleared; ensure the mixin returns a dict (not None).
# ConfigTemplate always forces autoescape off (#22652).
t = ConfigTemplate(name='empty', template_code='ok', environment_params=None)
self.assertEqual(t.get_environment_params(), {})
self.assertEqual(t.get_environment_params(), {'autoescape': False})
def test_get_environment_params_resolves_path_imports(self):
t = ConfigTemplate(
@ -1666,9 +1685,11 @@ class JinjaEnvironmentParamsIntegrationTestCase(TestCase):
self.assertEqual(template.environment_params['undefined'], 'jinja2.StrictUndefined')
def test_none_environment_params(self):
# ConfigTemplate always forces autoescape off (#22652).
template = self._make_template(None)
self.assertEqual(template.get_environment_params(), {})
self.assertEqual(template.get_environment_params(), {'autoescape': False})
def test_empty_environment_params(self):
# ConfigTemplate always forces autoescape off (#22652).
template = self._make_template({})
self.assertEqual(template.get_environment_params(), {})
self.assertEqual(template.get_environment_params(), {'autoescape': False})