讓咱們一塊兒來構建一個模板引擎(四)

上篇文章 中咱們的模板引擎實現了對 includeextends 的支持, 到此爲止咱們已經實現了模板引擎所需的大部分功能。 在本文中咱們將解決一些用於生成 html 的模板引擎須要面對的一些安全問題。html

轉義

首先要解決的就是轉義問題。到目前爲止咱們的模板引擎並無對變量和表達式結果進行轉義處理, 若是用於生成 html 源碼的話就會出現下面這樣的問題 ( template3c.py ):python

>>> from template3c import Template
>>> t = Template('<h1>{{ title }}</h1>')
>>> t.render({'title': 'hello<br />world'})
'<h1>hello<br />world</h1>'

很明顯 title 中包含的標籤須要被轉義,否則就會出現非預期的結果。 這裏咱們只對 & " ' > < 這幾個字符作轉義處理,其餘的字符可根據須要進行處理。git

html_escape_table = {
    '&': '&amp;',
    '"': '&quot;',
    '\'': '&apos;',
    '>': '&gt;',
    '<': '&lt;',
}


def html_escape(text):
    return ''.join(html_escape_table.get(c, c) for c in text)

轉義效果:github

>>> html_escape('hello<br />world')
'hello&lt;br /&gt;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 ):app

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 = {
    '&': '&amp;',
    '"': '&quot;',
    '\'': '&apos;',
    '>': '&gt;',
    '<': '&lt;',
}


def html_escape(text):
    return ''.join(html_escape_table.get(c, c) for c in 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)

效果:函數

>>> from template4a import Template
>>> t = Template('<h1>{{ title }}</h1>')
>>> t.render({'title': 'hello<br />world'})
'<h1>hello&lt;br /&gt;world</h1>'

>>> t = Template('<h1>{{ noescape(title) }}</h1>')
>>> t.render({'title': 'hello<br />world'})
'<h1>hello<br />world</h1>'
>>>

exec 的安全問題

因爲咱們的模板引擎是使用 exec 函數來執行生成的代碼的,全部就須要注意一下 exec 函數的安全問題,預防可能的服務端模板注入攻擊(詳見 使用 exec 函數時須要注意的一些安全問題 )。ui

首先要限制的是在模板中使用內置函數和執行時上下文變量( template4b.py ):spa

class Template:
    ...

    def render(self, context=None):
        """渲染模版"""
        namespace = {}
        namespace.update(self.default_context)
        namespace.setdefault('__builtins__', {})   # <---
        if context:
            namespace.update(context)
        exec(str(self.code_builder), namespace)
        result = namespace[self.func_name]()
        return result

效果:code

>>> from template4b import Template
>>> t = Template('{{ open("/etc/passwd").read() }}')
>>> t.render()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/mg/develop/lsbate/part4/template4b.py", line 245, in render
    result = namespace[self.func_name]()
  File "<string>", line 3, in __func_name
NameError: name 'open' is not defined

而後就是要限制經過其餘方式調用內置函數的行爲:

>>> from template4b import Template
>>> t = Template('{{ escape.__globals__["__builtins__"]["open"]("/etc/passwd").read()[0] }}')
>>> t.render()
'#'
>>>
>>> t = Template("{{ [x for x in [].__class__.__base__.__subclasses__() if x.__name__ == '_wrap_close'][0].__init__.__globals__['path'].os.system('date') }}")
>>> t.render()
Mon May 30 22:10:46 CST 2016
'0'

一種解決辦法就是不容許在模板中訪問如下劃線 _ 開頭的屬性。 爲何要包括單下劃線呢,由於約定單下劃線開頭的屬性是約定的私有屬性, 不該該在外部訪問這些屬性。

這裏咱們使用 dis 模塊來幫助咱們解析生成的代碼,而後再找出其中的特殊屬性
這裏咱們使用 tokenize 模塊來幫助咱們解析生成的代碼,而後再找出其中的特殊屬性。

import io
    import tokenize


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

        def render(self, ...):
            ...
            code = str(self.code_builder)
            if self.safe_attribute:
                check_unsafe_attributes(code)
            exec(code, namespace)
            func = namespace[self.func_name]

    def check_unsafe_attributes(s):
        g = tokenize.tokenize(io.BytesIO(s.encode('utf-8')).readline)
        pre_op = ''
        for toktype, tokval, _, _, _ in g:
            if toktype == tokenize.NAME and pre_op == '.' and \
                    tokval.startswith('_'):
                attr = tokval
                msg = "access to attribute '{0}' is unsafe.".format(attr)
                raise AttributeError(msg)
            elif toktype == tokenize.OP:
                pre_op = tokval

效果:

>>> 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 "<stdin>", line 1, in <module>
  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('<h1>{{ title }}</h1>')
>>> t.render({'title': 'hello<br />world'})
'<h1>hello&lt;br /&gt;world</h1>'

這個系列的文章到目前爲止就已經所有完成了。

若是你們感興趣的話能夠嘗試使用另外的方式來解析模板內容, 即: 使用詞法分析/語法分析的方式來解析模板內容(歡迎分享實現過程)。

P.S. 整個系列的全部文章地址:

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

相關文章
相關標籤/搜索