Coverage for utilities/jinja2.py: 29%
69 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import fnmatch
2import os
3import re
5from django.apps import apps
6from django.core.exceptions import ValidationError
7from django.utils.translation import gettext_lazy as _
8from jinja2 import BaseLoader, TemplateNotFound
9from jinja2.exceptions import TemplateSyntaxError
10from jinja2.meta import find_referenced_templates
11from jinja2.sandbox import SandboxedEnvironment
13from netbox.config import get_config
14from netbox.registry import registry
16__all__ = (
17 'DEFAULT_JINJA2_FILTERS',
18 'HTTP_HEADER_INVALID_CHARS_RE',
19 'JINJA2_TEMPLATE_RE',
20 'DataFileLoader',
21 'env_filter',
22 'render_jinja2',
23 'sanitize_http_header',
24 'validate_jinja2_syntax',
25)
27# Control characters (C0 range plus DEL) which are invalid in an HTTP header value. Notably, this includes the
28# carriage return and line feed characters used to smuggle additional headers (CR/LF injection).
29HTTP_HEADER_INVALID_CHARS_RE = re.compile(r'[\x00-\x1f\x7f]')
31# Matches the start of a Jinja2 expression, statement, or comment ({{, {%, {#), to detect whether a
32# template-capable field (e.g. Webhook.payload_url) is being used as a literal value or a template.
33JINJA2_TEMPLATE_RE = re.compile(r'\{[{%#]')
36def env_filter(name):
37 """
38 Jinja2 filter which returns the value of an environment variable, provided its name matches one of the patterns
39 listed in the JINJA_ENVIRONMENT_PARAMS configuration parameter. Patterns may include wildcards. Returns None if the
40 variable is not defined or its name does not match an allowed pattern.
41 """
42 patterns = get_config().JINJA_ENVIRONMENT_PARAMS or []
43 if not any(fnmatch.fnmatchcase(name, pattern) for pattern in patterns):
44 return None
45 return os.environ.get(name)
48def sanitize_http_header(value):
49 """
50 Jinja2 filter which sanitizes a value for safe inclusion in a raw HTTP header by stripping newlines and other
51 control characters. This guards against HTTP header (CR/LF) injection when interpolating untrusted data (e.g.
52 user-controlled object attributes) into a webhook's additional headers.
53 """
54 return HTTP_HEADER_INVALID_CHARS_RE.sub('', str(value))
57DEFAULT_JINJA2_FILTERS = {
58 'env': env_filter,
59}
62class DataFileLoader(BaseLoader):
63 """
64 Custom Jinja2 loader to facilitate populating template content from DataFiles.
65 """
66 def __init__(self, data_source):
67 self.data_source = data_source
68 self._template_cache = {}
70 def get_source(self, environment, template):
71 DataFile = apps.get_model('core', 'DataFile')
73 # Retrieve template content from cache
74 try:
75 template_source = self._template_cache[template]
76 except KeyError:
77 raise TemplateNotFound(template)
79 # Find and pre-fetch referenced templates
80 if referenced_templates := tuple(find_referenced_templates(environment.parse(template_source))):
81 related_files = DataFile.objects.filter(source=self.data_source)
82 # None indicates the use of dynamic resolution. If dependent files are statically
83 # defined, we can filter by path for optimization.
84 if None not in referenced_templates:
85 related_files = related_files.filter(path__in=referenced_templates)
86 self.cache_templates({
87 df.path: df.data_as_string for df in related_files
88 })
90 return template_source, template, lambda: True
92 def cache_templates(self, templates):
93 self._template_cache.update(templates)
96#
97# Utility functions
98#
100def _jinja2_filters(filters=None):
101 """
102 Build the Jinja2 filter table: default < plugin-registered < instance JINJA_FILTERS < filters
103 passed for this call, in increasing precedence. Instance-level config wins over
104 plugin-registered filters so site admins can override anything. Filters passed for this call
105 take precedence over all of them, so that context-specific (e.g. sanitization) filters cannot
106 be shadowed. Shared by render_jinja2() and validate_jinja2_syntax() so both see an identical
107 filter table.
108 """
109 return {
110 **DEFAULT_JINJA2_FILTERS,
111 **registry['plugins'].get('jinja_filters', {}),
112 **get_config().JINJA_FILTERS,
113 **(filters or {}),
114 }
117def render_jinja2(template_code, context, environment_params=None, data_file=None, debug=False, filters=None):
118 """
119 Render a Jinja2 template with the provided context. Return the rendered content.
121 If debug is True, the Jinja2 debug extension is enabled to assist with template development.
123 The optional `filters` argument is a mapping of additional Jinja2 filters to make available for this render only
124 (e.g. context-specific sanitization filters). These take precedence over the default and user-configured filters.
125 """
126 environment_params = dict(environment_params or {})
128 if debug:
129 extensions = list(environment_params.get('extensions', []))
130 if 'jinja2.ext.debug' not in extensions:
131 extensions.append('jinja2.ext.debug')
132 environment_params['extensions'] = extensions
134 if 'loader' not in environment_params:
135 if data_file:
136 loader = DataFileLoader(data_file.source)
137 loader.cache_templates({
138 data_file.path: template_code
139 })
140 else:
141 loader = BaseLoader()
142 environment_params['loader'] = loader
144 environment = SandboxedEnvironment(**environment_params)
145 environment.filters.update(_jinja2_filters(filters))
147 if data_file:
148 template = environment.get_template(data_file.path)
149 else:
150 template = environment.from_string(source=template_code)
151 return template.render(**context)
154def validate_jinja2_syntax(template_code, filters=None):
155 """
156 Validate that template_code is syntactically well-formed Jinja2 -- including that any filters
157 it references are registered -- without rendering it, so no context data is required. Pass the
158 same `filters` used at render time (see render_jinja2()) for an identical filter table. Raises
159 django.core.exceptions.ValidationError on failure.
160 """
161 environment = SandboxedEnvironment(loader=BaseLoader())
162 environment.filters.update(_jinja2_filters(filters))
163 try:
164 environment.compile(template_code)
165 except TemplateSyntaxError as e:
166 raise ValidationError(_("Invalid template: {error}").format(error=e))