让我们一起来构建一个模板引擎(四)

在 上篇文章 中我们的模板引擎实现了对 include 和 extends 的支持, 到此为止我们已经实现了模板引擎所需的大部分功能。 在本文中我们将解决一些用于生成 html 的模板引擎需要面对的一些安全问题。

转义

首先要解决的就是转义问题。到目前为止我们的模板引擎并没有对变量和表达式结果进行转义处理, 如果用于生成 html 源码的话就会出现下面这样的问题 ( template3c.py ):

from template3c import Template
t = Template('# {{ title }}')
t.render({'title': 'hello[br]world'})
'# hello[br]world'
很明显 title 中包含的标签需要被转义,不然就会出现非预期的结果。 这里我们只对 & " ' > 这几个字符做转义处理,其他的字符可根据需要进行处理。

html_escape_table = {
'&': '&',
'"': '"',
'\'': ''',
'>': '>',
'
转义效果:

html_escape('hello[br]world')
'hello[br]world'
既然有转义自然也要有禁止转义的功能,毕竟不能一刀切否则就丧失灵活性了。

class NoEscape:

def __init__(self, raw_text):    self.raw_text = raw_text

def escape(text):
if isinstance(text, NoEscape):
return str(text.raw_text)
else:
text = str(text)
return html_escape(text)

def noescape(text):
return NoEscape(text)
最终我们的模板引擎针对转义所做的修改如下(可以下载 template4a.py ):

class Template:
def init(self, ..., auto_escape=True):
...
self.auto_escape = auto_escape
self.default_context.setdefault('escape', escape)
self.default_context.setdefault('noescape', noescape)
...

def _handle_variable(self, token):    if self.auto_escape:        self.buffered.append('escape({})'.format(variable))    else:        self.buffered.append('str({})'.format(variable))def _parse_another_template_file(self, filename):    ...    template = self.__class__(            ...,            auto_escape=self.auto_escape    )    ...

class NoEscape:
def init(self, raw_text):
self.raw_text = raw_text

html_escape_table = {
'&': '&',
'"': '"',
'\'': ''',
'>': '>',
'
效果:

from template4a import Template
t = Template('# {{ title }}')
t.render({'title': 'hello[br]world'})
'# hello[br]world'

t = Template('# {{ noescape(title) }}')
t.render({'title': 'hello[br]world'})
'# hello[br]world'

exec 的安全问题

由于我们的模板引擎是使用 exec 函数来执行生成的代码的,所有就需要注意一下 exec 函数的安全问题,预防可能的服务端模板注入攻击(详见 使用 exec 函数时需要注意的一些安全问题 )。

首先要限制的是在模板中使用内置函数和执行时上下文变量( template4b.py ):

class Template:
...

def render(self, context=None):    """渲染模版"""    namespace = {}    namespace.update(self.default_context)    namespace.setdefault('__builtins__', {})   # 

效果:

from template4b import Template
t = Template('{{ open("/etc/passwd").read() }}')
t.render()
Traceback (most recent call last):
File "", line 1, in
File "/Users/mg/develop/lsbate/part4/template4b.py", line 245, in render
result = namespace[self.func_name]()
File "", line 3, in __func_name
NameError: name 'open' is not defined
然后就是要限制通过其他方式调用内置函数的行为:

from template4b import Template
t = Template('{{ escape.globals["builtins"]"open".read()[0] }}')
t.render()
'# '

t = Template("{{ [x for x in [].class.base.subclasses() if x.name == '_wrapclose'][0].init.globals['path'].os.system('date') }}")
t.render()
Mon May 30 22:10:46 CST 2016
'0'
一种解决办法就是不允许在模板中访问以下划线
开头的属性。 为什么要包括单下划线呢,因为约定单下划线开头的属性是约定的私有属性, 不应该在外部访问这些属性。

这里我们使用 dis 模块来帮助我们解析生成的代码,然后再找出其中的特殊属性(最新更新:dist 无法分析嵌套函数的代码,正在查找更安全的办法)。

import dis
import io

class Template:
def init(self, ..., safe_attribute=True):
...
self.safe_attribute = safe_attribute

def render(self, ...):    ...    func = namespace[self.func_name]    if self.safe_attribute:        check_unsafe_attributes(func)    result = func()

def check_unsafe_attributes(code):
writer = io.StringIO()
dis.dis(code, file=writer)
output = writer.getvalue()

match = re.search(r'\d+\s+LOAD_ATTR\s+\d+\s+\((?P_[^\)]+)\)',                  output)if match is not None:    attr = match.group('attr')    msg = "access to attribute '{0}' is unsafe.".format(attr)    raise AttributeError(msg)

效果:

from template4c import Template
t = Template("{{ [x for x in [].class.base.subclasses() if x.name == '_wrap_close'][0].init.globals['path'].os.system('date') }}")
t.render()
Traceback (most recent call last):
File "", line 1, in
File "/xxx/lsbate/part4/template4c.py", line 250, in render
check_unsafe_attributes(func)
File "/xxx/lsbate/part4/template4c.py", line 296, in check_unsafe_attributes
raise AttributeError(msg)
AttributeError: access to attribute 'class' is unsafe.

t = Template('# {{ title }}')
t.render({'title': 'hello[br]world'})
'# hello[br]world'
这个系列的文章到目前为止就已经全部完成了。

如果大家感兴趣的话可以尝试使用另外的方式来解析模板内容, 即: 使用词法分析/语法分析的方式来解析模板内容(欢迎分享实现过程)。

P.S. 整个系列的所有文章地址:

  1. 让我们一起来构建一个模板引擎(一)

  2. 让我们一起来构建一个模板引擎(二)

  3. 让我们一起来构建一个模板引擎(三)

  4. 让我们一起来构建一个模板引擎(四)

P.S. 文章中涉及的代码已经放到 GitHub 上了: https://github.com/mozillazg/lsbate

关键字:Python, 模板引擎


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部