资讯专栏INFORMATION COLUMN

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

yuxue / 2324人阅读

摘要:在本文中我们将解决一些用于生成的模板引擎需要面对的一些安全问题。整个系列的所有文章地址让我们一起来构建一个模板引擎一让我们一起来构建一个模板引擎二让我们一起来构建一个模板引擎三让我们一起来构建一个模板引擎四文章中涉及的代码已经放到上了

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

转义

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

>>> from template3c import Template
>>> t = Template("

{{ title }}

") >>> t.render({"title": "hello
world"}) "

hello
world

"

很明显 title 中包含的标签需要被转义,不然就会出现非预期的结果。 这里我们只对 & " " > < 这几个字符做转义处理,其他的字符可根据需要进行处理。

html_escape_table = {
    "&": "&",
    """: """,
    """: "'",
    ">": ">",
    "<": "<",
}


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

转义效果:

>>> html_escape("hello
world") "hello
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 = {
    "&": "&",
    """: """,
    """: "'",
    ">": ">",
    "<": "<",
}


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("

{{ title }}

") >>> t.render({"title": "hello
world"}) "

hello
world

" >>> t = Template("

{{ noescape(title) }}

") >>> t.render({"title": "hello
world"}) "

hello
world

" >>>
exec 的安全问题

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

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

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

效果:

>>> 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"]("/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 "", 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
world"}) "

hello
world

"

这个系列的文章到目前为止就已经全部完成了。

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

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

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

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

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

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/45470.html

相关文章

  • 我们起来构建模板引擎(三)

    摘要:在上篇文章中我们的模板引擎实现了对和对支持,同时在文章的最后我给大家留了一个问题如何实现支持和的标签功能。在本篇文章中我们将一起来动手实现这两个功能。 在 上篇文章 中我们的模板引擎实现了对 if 和 for 对支持,同时在文章的最后我给大家留了一个 问题:如何实现支持 include 和 extends 的标签功能。 在本篇文章中我们将一起来动手实现这两个功能。 include in...

    3fuyu 评论0 收藏0
  • webpack多页应用架构系列(十五):论前端如何在后端渲染开发模式下夹缝生存

    摘要:回到纯静态页面开发阶段,让页面不需要后端渲染也能跑起来。改造开始本文着重介绍如何将静态页面改造成后端渲染需要的模板。总结在后端渲染的项目里使用多页应用架构是绝对可行的,可不要给老顽固们吓唬得又回到传统前端架构了。 本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。原文地址:https://segmentfault.com/a/119000000820338...

    dinfer 评论0 收藏0
  • webpack多页应用架构系列(十五):论前端如何在后端渲染开发模式下夹缝生存

    摘要:回到纯静态页面开发阶段,让页面不需要后端渲染也能跑起来。改造开始本文着重介绍如何将静态页面改造成后端渲染需要的模板。总结在后端渲染的项目里使用多页应用架构是绝对可行的,可不要给老顽固们吓唬得又回到传统前端架构了。 本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。原文地址:https://segmentfault.com/a/119000000820338...

    dingda 评论0 收藏0
  • 基于TmodJS的前端模板工程化解决方案

    摘要:原作者唐斌腾讯什么原名是一个简单易用的前端模板预编译工具。本文作者为来自腾讯团队的唐斌,他在本文中为我们分析了传统前端模板内嵌的弊端,如开发调试效率低下自动化构建复杂度比较高等特点,并针对目前现状给出了较好的解决方案。 原作者: 唐斌(腾讯)| TmodJS什么 TmodJS(原名atc)是一个简单易用的前端模板预编译工具。它通过预编译技术让前端模板突破浏览器限制,实现后端模板一样的同...

    zhaochunqi 评论0 收藏0
  • 我们起来构建模板引擎

    摘要:使用技术我们将使用将模板编译为代码的方式来解析和渲染模板。下面我们就一起来实现这个方法。 假设我们要生成下面这样的 html 字符串: welcome, Tom age: 20 weight: 100 height: 170 要求姓名以及 中的内容是根据变量动态生成的,也就是这样的: welco...

    zombieda 评论0 收藏0

发表评论

0条评论

yuxue

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<