Adds Django template linter.
This adds Django template linter that would check for all trans/blocktrans tag expressions for escaping. PROD-465
This commit is contained in:
@@ -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='<span class="a">'|safe end_span='</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='<span class="a">'|safe end_span='</span>'|safe %}
|
||||
""", 'rule': ruleset.django_html_interpolation_missing},
|
||||
|
||||
{'expression': """
|
||||
{% trans "{span_start} whatever {span_end}" as tmsg %}
|
||||
{% interpolate_html tmsg start_span='<span class="a">'|unknown end_span='</span>'|safe %}
|
||||
""", 'rule': ruleset.django_html_interpolation_missing_safe_filter},
|
||||
|
||||
{'expression': """
|
||||
{% trans "<span 'a'='b' 'c'='d'> whatever </span>" as tmsg %}
|
||||
""", 'rule': ruleset.django_html_interpolation_missing},
|
||||
|
||||
{'expression': """
|
||||
{% trans "<span 'a'='b' 'c'='d'> whatever </span>" %}
|
||||
""", '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 <span 'a'='b' 'c'='d'> whatever </span>
|
||||
{% 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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
402
scripts/xsslint/xsslint/django_linter.py
Normal file
402
scripts/xsslint/xsslint/django_linter.py
Normal file
@@ -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"""(?<!{){(?!{)[a-zA-Z0-9 =\-'_":]+(?<!})}(?!})""", trans_expr, re.I)
|
||||
|
||||
if var_tags:
|
||||
return True
|
||||
|
||||
|
||||
def _is_html_interpolated(trans_var_name_used, expressions):
|
||||
html_interpolated = False
|
||||
interpolate_tag_expr = None
|
||||
for expr in expressions:
|
||||
if isinstance(expr, HtmlInterpolateExpression):
|
||||
if expr.interpolated_string_var == trans_var_name_used:
|
||||
html_interpolated = True
|
||||
interpolate_tag_expr = expr
|
||||
|
||||
return interpolate_tag_expr, html_interpolated
|
||||
|
||||
|
||||
def _add_violations(results, rule_violation, self):
|
||||
results.violations.append(ExpressionRuleViolation(
|
||||
rule_violation, self
|
||||
))
|
||||
@@ -12,6 +12,7 @@ from xsslint import visitors
|
||||
from xsslint.reporting import ExpressionRuleViolation, FileResults, RuleViolation
|
||||
from xsslint.rules import RuleSet
|
||||
from xsslint.utils import Expression, ParseString, StringLines, is_skip_dir
|
||||
from xsslint.django_linter import TransExpression, BlockTransExpression, HtmlInterpolateExpression
|
||||
|
||||
|
||||
class BaseLinter(object):
|
||||
@@ -1484,3 +1485,156 @@ class MakoTemplateLinter(BaseLinter):
|
||||
start_index = expression.end_index
|
||||
expressions.append(expression)
|
||||
return expressions
|
||||
|
||||
|
||||
class DjangoTemplateLinter(BaseLinter):
|
||||
"""
|
||||
The linter for Django template files
|
||||
"""
|
||||
LINE_COMMENT_DELIM = "{#"
|
||||
|
||||
ruleset = RuleSet(
|
||||
django_trans_missing_escape='django-trans-missing-escape',
|
||||
django_trans_invalid_escape_filter='django-trans-invalid-escape-filter',
|
||||
django_trans_escape_variable_mismatch='django-trans-escape-variable-mismatch',
|
||||
django_blocktrans_missing_escape_filter='django-blocktrans-missing-escape-filter',
|
||||
django_blocktrans_parse_error='django-blocktrans-parse-error',
|
||||
django_blocktrans_escape_filter_parse_error='django-blocktrans-escape-filter-parse-error',
|
||||
django_html_interpolation_missing_safe_filter='django-html-interpolation-missing-safe-filter',
|
||||
django_html_interpolation_missing='django-html-interpolation-missing',
|
||||
django_html_interpolation_invalid_tag='django-html-interpolation-invalid-tag',
|
||||
)
|
||||
|
||||
def __init__(self, skip_dirs=None):
|
||||
"""
|
||||
Init method.
|
||||
"""
|
||||
super(DjangoTemplateLinter, self).__init__()
|
||||
self._skip_django_dirs = skip_dirs or ()
|
||||
|
||||
def process_file(self, directory, file_name):
|
||||
"""
|
||||
Process file to determine if it is a Django template file and
|
||||
if it is safe.
|
||||
Arguments:
|
||||
directory (string): The directory of the file to be checked
|
||||
file_name (string): A filename for a potential Django file
|
||||
Returns:
|
||||
The file results containing any violations.
|
||||
"""
|
||||
django_file_full_path = os.path.normpath(directory + '/' + file_name)
|
||||
results = FileResults(django_file_full_path)
|
||||
|
||||
if not results.is_file:
|
||||
return results
|
||||
|
||||
if not self._is_valid_directory(directory):
|
||||
return results
|
||||
|
||||
if not (file_name.lower().endswith('.html')):
|
||||
return results
|
||||
|
||||
return self._load_and_check_file_is_safe(django_file_full_path, self._check_django_file_is_safe, results)
|
||||
|
||||
def _is_valid_directory(self, directory):
|
||||
"""
|
||||
Determines if the provided directory is a directory that could contain
|
||||
Django template files that need to be linted.
|
||||
Arguments:
|
||||
directory: The directory to be linted.
|
||||
Returns:
|
||||
True if this directory should be linted for Django template violations
|
||||
and False otherwise.
|
||||
"""
|
||||
if is_skip_dir(self._skip_django_dirs, directory):
|
||||
return False
|
||||
|
||||
if ('/templates/' in directory) or directory.endswith('/templates'):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _is_django_template(self, django_template):
|
||||
"""
|
||||
Determines if the template is actually a Django template.
|
||||
Arguments:
|
||||
mako_template: The template code.
|
||||
Returns:
|
||||
True if this is really a Django template, and False otherwise.
|
||||
"""
|
||||
if re.search('({%.*%})|({{.*}})|({#.*#})', django_template) is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_django_file_is_safe(self, django_template, results):
|
||||
if not self._is_django_template(django_template):
|
||||
return
|
||||
self._check_django_expression(django_template, results)
|
||||
results.prepare_results(django_template, line_comment_delim=self.LINE_COMMENT_DELIM)
|
||||
|
||||
def _check_django_expression(self, django_template, results):
|
||||
"""
|
||||
Searches for django trans and blocktrans expression and then checks
|
||||
if they contain violations
|
||||
Arguments:
|
||||
django_template: The contents of the Django template.
|
||||
results: A list of results into which violations will be added.
|
||||
"""
|
||||
expressions = []
|
||||
self._find_django_expressions(django_template, results, expressions)
|
||||
for expr in expressions:
|
||||
expr.validate_expression(django_template, expressions)
|
||||
|
||||
def _find_django_expressions(self, django_template, results, expressions):
|
||||
"""
|
||||
Finds all the Django trans/blocktrans expressions in a Django template
|
||||
and creates a list of dicts for each expression.
|
||||
Arguments:
|
||||
django_template: The content of the Django template.
|
||||
Returns:
|
||||
A list of Expressions.
|
||||
"""
|
||||
|
||||
comments = list(re.finditer(r'{% comment .*%}', django_template, re.I))
|
||||
endcomments = list(re.finditer(r'{% endcomment .*%}', django_template, re.I))
|
||||
|
||||
trans_iterator = re.finditer(r'{% trans .*?%}', django_template, re.I)
|
||||
for t in trans_iterator:
|
||||
if self._check_expression_not_commented(t, comments, endcomments):
|
||||
continue
|
||||
trans_expr = TransExpression(self.ruleset, results, t.start(), t.end(),
|
||||
start_delim='{%', end_delim='%}',
|
||||
template=django_template)
|
||||
if trans_expr:
|
||||
expressions.append(trans_expr)
|
||||
|
||||
block_trans_iterator = re.finditer(r'{% blocktrans .*?%}', django_template, re.I)
|
||||
for bt in block_trans_iterator:
|
||||
if self._check_expression_not_commented(bt, comments, endcomments):
|
||||
continue
|
||||
trans_expr = BlockTransExpression(self.ruleset, results, bt.start(), bt.end(),
|
||||
start_delim='{%', end_delim='%}',
|
||||
template=django_template)
|
||||
if trans_expr:
|
||||
expressions.append(trans_expr)
|
||||
|
||||
interpolation_iterator = re.finditer(r'{% interpolate_html .*?%}', django_template, re.I)
|
||||
for it in interpolation_iterator:
|
||||
if self._check_expression_not_commented(it, comments, endcomments):
|
||||
continue
|
||||
trans_expr = HtmlInterpolateExpression(self.ruleset, results,
|
||||
it.start(), it.end(),
|
||||
start_delim='{%', end_delim='%}',
|
||||
template=django_template)
|
||||
if trans_expr:
|
||||
expressions.append(trans_expr)
|
||||
|
||||
def _check_expression_not_commented(self, expr, comments, endcomments):
|
||||
|
||||
for i in range(len(endcomments)):
|
||||
start_comment = comments[i]
|
||||
end_comment = endcomments[i]
|
||||
|
||||
if (expr.start() >= start_comment.start()) and \
|
||||
(expr.start() <= end_comment.start()):
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user