diff --git a/scripts/xsslint/tests/test_linters.py b/scripts/xsslint/tests/test_linters.py
index ea3a2eb66f..83fd6f3e5f 100644
--- a/scripts/xsslint/tests/test_linters.py
+++ b/scripts/xsslint/tests/test_linters.py
@@ -10,7 +10,11 @@ from unittest import TestCase
from ddt import data, ddt
from six.moves import range, zip
-from xsslint.linters import JavaScriptLinter, MakoTemplateLinter, PythonLinter, UnderscoreTemplateLinter
+from xsslint.linters import (
+ JavaScriptLinter, MakoTemplateLinter,
+ PythonLinter, UnderscoreTemplateLinter,
+ DjangoTemplateLinter
+)
from xsslint.reporting import FileResults
from xsslint.utils import ParseString
@@ -28,6 +32,10 @@ def _build_mako_linter():
)
+def _build_django_linter():
+ return DjangoTemplateLinter()
+
+
class TestLinter(TestCase):
"""
Test Linter base class
@@ -1443,3 +1451,255 @@ class TestMakoTemplateLinter(TestLinter):
start_inner_index = parse_string.start_index + parse_string.quote_length
end_inner_index = parse_string.end_index - parse_string.quote_length
self.assertEqual(data['template'][start_inner_index:end_inner_index], parse_string.string_inner)
+
+
+@ddt
+class TestDjangoTemplateLinter(TestLinter):
+ """
+ Test DjangoTemplateLinter
+ """
+
+ ruleset = (
+ DjangoTemplateLinter.ruleset
+ )
+ @data(
+
+ {'expression': '{% trans "whatever" as tmsg %}{{tmsg|force_escape}}',
+ 'rule': None},
+
+ {'expression': '{% trans "whatever" as tmsgx %}{{tmsg|force_escape}}',
+ 'rule': ruleset.django_trans_escape_variable_mismatch},
+
+ {'expression': '{% trans "whatever" as tmsgx %}{{tmsgx|force_escap}}',
+ 'rule': ruleset.django_trans_invalid_escape_filter},
+
+ {'expression': '{% trans "whatever" as tmsg %}',
+ 'rule': ruleset.django_trans_missing_escape},
+
+ {'expression': '{% trans "whatever" %}',
+ 'rule': ruleset.django_trans_missing_escape},
+
+ {'expression': '{% trans "{span_start} whatever {span_end}" as tmsg %}',
+ 'rule': ruleset.django_html_interpolation_missing},
+
+ {'expression': '{% trans "{span_start} whatever {span_end}" as tmsg %}{{tmsg|force_filter}}',
+ 'rule': ruleset.django_html_interpolation_missing},
+
+ {'expression': """
+ {% trans "{span_start} whatever {span_end}" as tmsg %}
+ {% interpolate_html tmsg user_name=user_data.name start_span=''|safe end_span=''|safe %}
+ """, 'rule': None},
+
+ {'expression': """
+ {% trans "{span_start} whatever {span_end}" as tmsg %}
+ {% interpolate_html %}
+ """, 'rule': [ruleset.django_html_interpolation_missing, ruleset.django_html_interpolation_invalid_tag]},
+
+ {'expression': """
+ {% trans "{span_start} whatever {span_end}" as tmsg %}
+ {% interpolate_html t start_span=''|safe end_span=''|safe %}
+ """, 'rule': ruleset.django_html_interpolation_missing},
+
+ {'expression': """
+ {% trans "{span_start} whatever {span_end}" as tmsg %}
+ {% interpolate_html tmsg start_span=''|unknown end_span=''|safe %}
+ """, 'rule': ruleset.django_html_interpolation_missing_safe_filter},
+
+ {'expression': """
+ {% trans " whatever " as tmsg %}
+ """, 'rule': ruleset.django_html_interpolation_missing},
+
+ {'expression': """
+ {% trans " whatever " %}
+ """, 'rule': ruleset.django_html_interpolation_missing},
+
+ {'expression': """
+ {% filter force_escape %}
+ {% blocktrans %}
+ Some translation string
+ {% endblocktrans %}
+ {% endfilter %}
+ """, 'rule': None},
+
+ {'expression': """
+ {% filter force_escape
+ {% blocktrans %}
+ Some translation string
+ {% endblocktrans %}
+ {% endfilter %}
+ """, 'rule': ruleset.django_blocktrans_escape_filter_parse_error},
+
+ {'expression': """
+ {% filter someother_filter %}
+ {% blocktrans %}
+ Some translation strubg
+ {% endblocktrans %}
+ {% endfilter %}
+ """, 'rule': ruleset.django_blocktrans_missing_escape_filter},
+
+ {'expression': """
+ {% filter force_escape xyz %}
+ {% blocktrans %}
+ Some translation string
+ {% endblocktrans %}
+ {% endfilter %}
+ """, 'rule': ruleset.django_blocktrans_missing_escape_filter},
+
+ {'expression': """
+ {% blocktrans %}
+ Some translation string
+ {% endblocktrans %}
+ """, 'rule': ruleset.django_blocktrans_missing_escape_filter},
+
+ {'expression': """
+ {% blocktrans %}
+ Some translation whatever
+ {% endblocktrans %}
+ """, 'rule': ruleset.django_html_interpolation_missing},
+
+ )
+ def test_check_django_expressions_in_html(self, data):
+ """
+ Test _check_django_file_is_safe in html context provides appropriate violations
+ """
+ linter = _build_django_linter()
+ results = FileResults('')
+
+ django_template = textwrap.dedent(
+ """
+ {load_i18n}
+ {load_django_html}
+ {expression}
+ """.format(expression=data['expression'],
+ load_i18n='{% load i18n %}',
+ load_django_html='{% load django_html %}'))
+
+ linter._check_django_file_is_safe(django_template, results)
+
+ self._validate_data_rules(data, results)
+
+ def test_check_django_trans_expression_disabled(self):
+ """
+ Test _check_django_file_is_safe with disable pragma results in no
+ violation
+ """
+ linter = _build_django_linter()
+ results = FileResults('')
+
+ django_template = textwrap.dedent(
+ """
+ {load_i18n}
+ {load_django_html}
+ {expression}
+ """.format(expression="""
+ {# xss-lint: disable=django-trans-missing-escape #}
+ {% trans 'Documentation' as tmsg%}
+ """,
+ load_i18n='{% load i18n %}',
+ load_django_html='{% load django_html %}'))
+
+ linter._check_django_file_is_safe(django_template, results)
+
+ self.assertEqual(len(results.violations), 1)
+ self.assertTrue(results.violations[0].is_disabled)
+
+ def test_check_django_blocktrans_expression_disabled(self):
+ """
+ Test _check_django_file_is_safe with disable pragma results in no
+ violation
+ """
+ linter = _build_django_linter()
+ results = FileResults('')
+
+ django_template = textwrap.dedent(
+ """
+ {load_i18n}
+ {load_django_html}
+ {expression}
+ """.format(expression="""
+ {# xss-lint: disable=django-blocktrans-missing-escape-filter #}
+ {% blocktrans %}
+ sometext
+ {% endblocktrans %}
+ """,
+ load_i18n='{% load i18n %}',
+ load_django_html='{% load django_html %}'))
+
+ linter._check_django_file_is_safe(django_template, results)
+
+ self.assertEqual(len(results.violations), 1)
+ self.assertTrue(results.violations[0].is_disabled)
+
+ def test_check_django_trans_expression_commented(self):
+ """
+ Test _check_django_file_is_safe with comment results in no
+ violation
+ """
+ linter = _build_django_linter()
+ results = FileResults('')
+
+ django_template = textwrap.dedent(
+ """
+ {load_i18n}
+ {load_django_html}
+ {expression}
+ """.format(expression="""
+ {# trans 'Documentation' as tmsg #}
+ """,
+ load_i18n='{% load i18n %}',
+ load_django_html='{% load django_html %}'))
+
+ linter._check_django_file_is_safe(django_template, results)
+
+ self.assertEqual(len(results.violations), 0)
+
+ def test_check_django_blocktrans_expression_commented(self):
+ """
+ Test _check_django_file_is_safe with comment results in no
+ violation
+ """
+ linter = _build_django_linter()
+ results = FileResults('')
+
+ django_template = textwrap.dedent(
+ """
+ {load_i18n}
+ {load_django_html}
+ {expression}
+ """.format(expression="""
+ {% comment %}
+ {% blocktrans %}
+ {% endblocktrans %}
+ {% endcomment %}
+ """,
+ load_i18n='{% load i18n %}',
+ load_django_html='{% load django_html %}'))
+
+ linter._check_django_file_is_safe(django_template, results)
+
+ self.assertEqual(len(results.violations), 0)
+
+ def test_check_django_interpolate_tag_expression_commented(self):
+ """
+ Test _check_django_file_is_safe with comment results in no
+ violation
+ """
+ linter = _build_django_linter()
+ results = FileResults('')
+
+ django_template = textwrap.dedent(
+ """
+ {load_i18n}
+ {load_django_html}
+ {expression}
+ """.format(expression="""
+ {% comment %}
+ {% interpolate_html %}
+ {% endcomment %}
+ """,
+ load_i18n='{% load i18n %}',
+ load_django_html='{% load django_html %}'))
+
+ linter._check_django_file_is_safe(django_template, results)
+
+ self.assertEqual(len(results.violations), 0)
diff --git a/scripts/xsslint/xsslint/default_config.py b/scripts/xsslint/xsslint/default_config.py
index e6926f6939..865d7a23ac 100644
--- a/scripts/xsslint/xsslint/default_config.py
+++ b/scripts/xsslint/xsslint/default_config.py
@@ -1,7 +1,11 @@
# Default xsslint config module.
from __future__ import absolute_import
-from xsslint.linters import JavaScriptLinter, MakoTemplateLinter, PythonLinter, UnderscoreTemplateLinter
+from xsslint.linters import (
+ JavaScriptLinter, MakoTemplateLinter,
+ PythonLinter, UnderscoreTemplateLinter,
+ DjangoTemplateLinter
+)
# Define the directories that should be ignored by the script.
SKIP_DIRS = (
@@ -45,6 +49,10 @@ MAKO_LINTER = MakoTemplateLinter(
skip_dirs=MAKO_SKIP_DIRS
)
+DJANGO_SKIP_DIRS = SKIP_DIRS
+DJANGO_LINTER = DjangoTemplateLinter(
+ skip_dirs=DJANGO_SKIP_DIRS
+)
# (Required) Define the linters (code-checkers) that should be run by the script.
-LINTERS = (MAKO_LINTER, UNDERSCORE_LINTER, JAVASCRIPT_LINTER, PYTHON_LINTER)
+LINTERS = (DJANGO_LINTER, MAKO_LINTER, UNDERSCORE_LINTER, JAVASCRIPT_LINTER, PYTHON_LINTER)
diff --git a/scripts/xsslint/xsslint/django_linter.py b/scripts/xsslint/xsslint/django_linter.py
new file mode 100644
index 0000000000..294b7274e2
--- /dev/null
+++ b/scripts/xsslint/xsslint/django_linter.py
@@ -0,0 +1,402 @@
+"""
+Classes for Django Template Linting.
+"""
+import re
+from xsslint.utils import Expression, StringLines
+from xsslint.reporting import ExpressionRuleViolation
+
+
+class TransExpression(Expression):
+ """
+ The expression handling trans tag
+ """
+
+ def __init__(self, ruleset, results, *args, **kwargs):
+ super(TransExpression, self).__init__(*args, **kwargs)
+ self.string_lines = StringLines(kwargs['template'])
+ self.ruleset = ruleset
+ self.results = results
+
+ def validate_expression(self, template_file, expressions=None):
+ """
+ Validates trans tag expression for missing escaping filter
+
+ Arguments:
+ template_file: The content of the Django template.
+ results: Violations to be generated.
+
+ Returns:
+ None
+ """
+ trans_expr = self.expression_inner
+
+ # extracting translation string message
+ trans_var_name_used, trans_expr_msg = self.process_translation_string(trans_expr)
+ if not trans_var_name_used or not trans_expr_msg:
+ return
+
+ # Checking if trans tag has interpolated variables eg {} in translations string.
+ # and testing for possible interpolate_html tag used for it.
+ if self.check_string_interpolation(trans_expr_msg,
+ trans_var_name_used,
+ expressions,
+ template_file):
+ return
+
+ escape_expr_start_pos, escape_expr_end_pos = self.find_filter_tag(template_file)
+ if not escape_expr_start_pos or not escape_expr_end_pos:
+ return
+
+ self.process_escape_filter_tag(template_file=template_file,
+ escape_expr_start_pos=escape_expr_start_pos,
+ escape_expr_end_pos=escape_expr_end_pos,
+ trans_var_name_used=trans_var_name_used)
+
+ def process_translation_string(self, trans_expr):
+ """
+ Process translation string into string and variable name used
+
+ Arguments:
+ trans_expr: Translation expression inside {% %}
+ Returns:
+ None
+ """
+
+ quote = re.search(r"""\s*['"].*['"]\s*""", trans_expr, re.I)
+ if not quote:
+ _add_violations(self.results,
+ self.ruleset.django_trans_escape_filter_parse_error,
+ self)
+ return None, None
+
+ trans_expr_msg = trans_expr[quote.start():quote.end()].strip()
+ if _check_is_string_has_html(trans_expr_msg):
+ _add_violations(self.results,
+ self.ruleset.django_html_interpolation_missing,
+ self)
+ return None, None
+
+ pos = trans_expr.find('as', quote.end())
+ if pos == -1:
+ _add_violations(self.results, self.ruleset.django_trans_missing_escape, self)
+ return None, None
+
+ trans_var_name_used = trans_expr[pos + len('as'):].strip()
+ return trans_var_name_used, trans_expr_msg
+
+ def check_string_interpolation(self, trans_expr_msg, trans_var_name_used, expressions, template_file):
+ """
+ Checks if the translation string has used interpolation variable eg {variable} but not
+ used interpolate_html tag to escape them
+
+ Arguments:
+ trans_expr_msg: Translation string in quotes
+ trans_var_name_used: Translation variable used
+ expressions: List of expressions found during django file processing
+ template_file: django template file
+ Returns:
+ True: In case it finds interpolated variables
+ False: No interpolation variables found
+ """
+
+ if _check_is_string_has_variables(trans_expr_msg):
+ interpolate_tag, html_interpolated = _is_html_interpolated(trans_var_name_used,
+ expressions)
+
+ if not html_interpolated:
+ _add_violations(self.results, self.ruleset.django_html_interpolation_missing, self)
+ if interpolate_tag:
+ interpolate_tag.validate_expression(template_file, expressions)
+ return True
+ return
+
+ def find_filter_tag(self, template_file):
+ """
+ Finds if there is force_filter tag applied
+
+ Arguments:
+ template_file: django template file
+ Returns:
+ (None, None): In case there is a violations
+ (start, end): Found filter tag start and end position
+ """
+
+ trans_expr_lineno = self.string_lines.index_to_line_number(self.start_index)
+ escape_expr_start_pos = template_file.find('{{', self.end_index)
+ if escape_expr_start_pos == -1:
+ _add_violations(self.results,
+ self.ruleset.django_trans_missing_escape,
+ self)
+ return None, None
+
+ # {{ found but should be on the same line as trans tag
+ trans_expr_filter_lineno = self.string_lines.index_to_line_number(escape_expr_start_pos)
+ if trans_expr_filter_lineno != trans_expr_lineno:
+ _add_violations(self.results,
+ self.ruleset.django_trans_missing_escape,
+ self)
+ return None, None
+
+ escape_expr_end_pos = template_file.find('}}', escape_expr_start_pos)
+ # couldn't find matching }}
+ if escape_expr_end_pos == -1:
+ _add_violations(self.results,
+ self.ruleset.django_trans_missing_escape,
+ self)
+ return None, None
+
+ # }} should be also on the same line
+ trans_expr_filter_lineno = self.string_lines.index_to_line_number(escape_expr_end_pos)
+ if trans_expr_filter_lineno != trans_expr_lineno:
+ _add_violations(self.results,
+ self.ruleset.django_trans_missing_escape,
+ self)
+ return None, None
+
+ return escape_expr_start_pos, escape_expr_end_pos
+
+ def process_escape_filter_tag(self, **kwargs):
+ """
+ Checks if the escape filter and process it for violations
+
+ Arguments:
+ kwargs: Having force_filter expression start, end, trans expression variable
+ used and templates
+ Returns:
+ None: If found any violations
+ """
+
+ template_file = kwargs['template_file']
+ escape_expr_start_pos = kwargs['escape_expr_start_pos']
+ escape_expr_end_pos = kwargs['escape_expr_end_pos']
+ trans_var_name_used = kwargs['trans_var_name_used']
+
+ escape_expr = template_file[escape_expr_start_pos + len('{{'):
+ escape_expr_end_pos].strip(' ')
+
+ # check escape expression has the right variable and its escaped properly
+ # with force_escape filter
+ if '|' not in escape_expr or len(escape_expr.split('|')) != 2:
+ _add_violations(self.results,
+ self.ruleset.django_trans_invalid_escape_filter,
+ self)
+ return
+
+ escape_expr_var_used, escape_filter = \
+ escape_expr.split('|')[0].strip(' '), escape_expr.split('|')[1].strip(' ')
+ if trans_var_name_used != escape_expr_var_used:
+ _add_violations(self.results,
+ self.ruleset.django_trans_escape_variable_mismatch,
+ self)
+ return
+
+ if escape_filter != 'force_escape':
+ _add_violations(self.results,
+ self.ruleset.django_trans_invalid_escape_filter,
+ self)
+ return
+
+
+class BlockTransExpression(Expression):
+ """
+ The expression handling blocktrans tag
+ """
+ def __init__(self, ruleset, results, *args, **kwargs):
+ super(BlockTransExpression, self).__init__(*args, **kwargs)
+ self.string_lines = StringLines(kwargs['template'])
+ self.ruleset = ruleset
+ self.results = results
+
+ def validate_expression(self, template_file, expressions=None):
+ """
+ Validates blocktrans tag expression for missing escaping filter
+
+ Arguments:
+ template_file: The content of the Django template.
+ results: Violations to be generated.
+
+ Returns:
+ None
+ """
+ if not self._process_block(template_file, expressions):
+ return
+
+ filter_start_pos = template_file.rfind('{%', 0, self.start_index)
+ if filter_start_pos == -1:
+ _add_violations(self.results,
+ self.ruleset.django_blocktrans_missing_escape_filter,
+ self)
+ return
+
+ filter_end_pos = template_file.find('%}', filter_start_pos)
+ if filter_end_pos > self.start_index:
+ _add_violations(self.results,
+ self.ruleset.django_blocktrans_escape_filter_parse_error,
+ self)
+ return
+
+ escape_filter = template_file[filter_start_pos:filter_end_pos + 2]
+
+ if len(escape_filter) < len('{%filter force_escape%}'):
+ _add_violations(self.results,
+ self.ruleset.django_blocktrans_missing_escape_filter,
+ self)
+ return
+
+ escape_filter = escape_filter[2:-2].strip()
+ escape_filter = escape_filter.split(' ')
+
+ if len(escape_filter) != 2:
+ _add_violations(self.results,
+ self.ruleset.django_blocktrans_missing_escape_filter,
+ self)
+ return
+
+ if escape_filter[0] != 'filter' or escape_filter[1] != 'force_escape':
+ _add_violations(self.results,
+ self.ruleset.django_blocktrans_missing_escape_filter,
+ self)
+ return
+
+ def _process_block(self, template_file, expressions):
+ """
+ Process blocktrans..endblocktrans block
+
+ Arguments:
+ template_file: The content of the Django template.
+
+ Returns:
+ None
+ """
+ blocktrans_string = self._extract_translation_msg(template_file)
+
+ # if no string extracted might have hit a parse error just return
+ if not blocktrans_string:
+ return
+
+ if _check_is_string_has_html(blocktrans_string):
+ _add_violations(self.results, self.ruleset.django_html_interpolation_missing, self)
+ return
+
+ # Checking if blocktrans tag has interpolated variables eg {}
+ # in translations string. Would be tested for
+ # possible html interpolation done somewhere else.
+
+ if _check_is_string_has_variables(blocktrans_string):
+ blocktrans_expr = self.expression_inner
+ pos = blocktrans_expr.find('asvar')
+ if pos == -1:
+ _add_violations(self.results, self.ruleset.django_html_interpolation_missing, self)
+ return
+
+ trans_var_name_used = blocktrans_expr[pos + len('asvar'):].strip()
+
+ # check for interpolate_html expression for the variable in trans expression
+ interpolate_tag, html_interpolated = _is_html_interpolated(trans_var_name_used,
+ expressions)
+ if not html_interpolated:
+ _add_violations(self.results, self.ruleset.django_html_interpolation_missing, self)
+ if interpolate_tag:
+ interpolate_tag.validate_expression(template_file, expressions)
+ return
+ return True
+
+ def _extract_translation_msg(self, template_file):
+
+ endblocktrans = re.compile(r'{%\s*endblocktrans.*?%}').search(template_file,
+ self.end_index)
+ if not endblocktrans.start():
+ _add_violations(self.results,
+ self.ruleset.django_blocktrans_parse_error,
+ self)
+ return
+
+ return template_file[self.end_index + 2: endblocktrans.start()].strip(' ')
+
+
+class HtmlInterpolateExpression(Expression):
+ """
+ The expression handling interplate_html tag
+ """
+ def __init__(self, ruleset, results, *args, **kwargs):
+ super(HtmlInterpolateExpression, self).__init__(*args, **kwargs)
+ self.string_lines = StringLines(kwargs['template'])
+ self.ruleset = ruleset
+ self.results = results
+ self.validated = False
+ self.interpolated_string_var = None
+
+ trans_expr = self.expression_inner
+ # extracting interpolated variable string name
+ expr_list = trans_expr.split(' ')
+ if len(expr_list) < 2:
+ _add_violations(self.results,
+ self.ruleset.django_html_interpolation_invalid_tag,
+ self)
+ return
+ self.interpolated_string_var = expr_list[1]
+
+ def validate_expression(self, template_file, expressions=None):
+ """
+ Validates interpolate_html tag expression for missing safe filter for html tags
+
+ Arguments:
+ template_file: The content of the Django template.
+ results: Violations to be generated.
+
+ Returns:
+ None
+ """
+
+ # if the expression is already validated, we would not be processing it again
+ if not self.interpolated_string_var or self.validated:
+ return
+
+ self.validated = True
+ trans_expr = self.expression_inner
+
+ html_tags = re.finditer(r"""\s*['"]?[a-zA-Z0-9 =\-'_"]+.*?>['"]""",
+ trans_expr, re.I)
+ for html_tag in html_tags:
+ tag_end = html_tag.end()
+
+ escape_filter = trans_expr[tag_end:tag_end + len('|safe')]
+ if escape_filter != '|safe':
+ _add_violations(self.results,
+ self.ruleset.django_html_interpolation_missing_safe_filter,
+ self)
+ return
+
+ return True
+
+
+def _check_is_string_has_html(trans_expr):
+ html_tags = re.search(r"""?[a-zA-Z0-9 =\-'_":]+>""", trans_expr, re.I)
+
+ if html_tags:
+ return True
+
+
+def _check_is_string_has_variables(trans_expr):
+ var_tags = re.search(r"""(?= start_comment.start()) and \
+ (expr.start() <= end_comment.start()):
+ return True