在 上篇文章 中咱們的模板引擎實現了對 include
和 extends
的支持, 到此爲止咱們已經實現了模板引擎所需的大部分功能。 在本文中咱們將解決一些用於生成 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 = { '&': '&', '"': '"', '\'': ''', '>': '>', '<': '<', } def html_escape(text): return ''.join(html_escape_table.get(c, c) for c in text)
轉義效果:github
>>> 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 ):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 = { '&': '&', '"': '"', '\'': ''', '>': '>', '<': '<', } 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<br />world</h1>' >>> t = Template('<h1>{{ noescape(title) }}</h1>') >>> t.render({'title': 'hello<br />world'}) '<h1>hello<br />world</h1>' >>>
因爲咱們的模板引擎是使用 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<br />world</h1>'
這個系列的文章到目前爲止就已經所有完成了。
若是你們感興趣的話能夠嘗試使用另外的方式來解析模板內容, 即: 使用詞法分析/語法分析的方式來解析模板內容(歡迎分享實現過程)。
P.S. 整個系列的全部文章地址:
P.S. 文章中涉及的代碼已經放到 GitHub 上了: https://github.com/mozillazg/lsbate