Răsfoiți Sursa

feat: support XSS sanitizer when output html and markdown

wangweimin 4 ani în urmă
părinte
comite
f85722fa72

+ 10 - 2
docs/spec.rst

@@ -188,9 +188,17 @@ output
 
 ``type`` 的可选值及特有字段:
 
-* type: markdown, html
+* type: markdown
+
+  * content: str
+  * options: dict, `marked.js <https://github.com/markedjs/marked>`_ 选项
+  * sanitize: bool, 是否使用 `DOMPurify <https://github.com/cure53/DOMPurify>`_ 对内容进行过滤来防止XSS攻击。
+
+* type: html
+
+  * content: str:
+  * sanitize: bool, 是否使用 `DOMPurify <https://github.com/cure53/DOMPurify>`_ 对内容进行过滤来防止XSS攻击。
 
-  * content: str 输出内容的原始字符串
 
 * type: text
 

+ 1 - 0
pywebio/html/index.html

@@ -46,6 +46,7 @@
 <script src="js/bootstrap.min.js"></script>
 <script src="js/toastify.min.js"></script> <!-- toast -->
 <script src="js/bs-custom-file-input.min.js"></script> <!-- bootstrap custom file input-->
+<script src="js/purify.min.js"></script>  <!-- XSS sanitizer -->
 
 <script src="js/pywebio.min.js"></script>
 

Fișier diff suprimat deoarece este prea mare
+ 1 - 0
pywebio/html/js/purify.min.js


+ 10 - 7
pywebio/output.py

@@ -317,19 +317,20 @@ def put_text(*texts, sep=' ', inline=False, scope=Scope.Current, position=Output
     return Output(spec)
 
 
-def put_html(html, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
+def put_html(html, sanitize=False, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """
     输出Html内容。
 
     与支持通过Html输出内容到 `Jupyter Notebook <https://nbviewer.jupyter.org/github/ipython/ipython/blob/master/examples/IPython%20Kernel/Rich%20Output.ipynb#HTML>`_ 的库兼容。
 
     :param html: html字符串或实现了 `IPython.display.HTML` 接口的实例
+    :param bool sanitize: 是否使用 `DOMPurify <https://github.com/cure53/DOMPurify>`_ 对内容进行过滤来防止XSS攻击。
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
     """
     if hasattr(html, '__html__'):
         html = html.__html__()
 
-    spec = _get_output_spec('html', content=html, scope=scope, position=position)
+    spec = _get_output_spec('html', content=html, sanitize=sanitize, scope=scope, position=position)
     return Output(spec)
 
 
@@ -353,7 +354,7 @@ def put_code(content, language='', scope=Scope.Current, position=OutputPosition.
     return put_markdown(code, scope=scope, position=position)
 
 
-def put_markdown(mdcontent, strip_indent=0, lstrip=False, options=None,
+def put_markdown(mdcontent, strip_indent=0, lstrip=False, options=None, sanitize=True,
                  scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """
     输出Markdown内容。
@@ -363,6 +364,7 @@ def put_markdown(mdcontent, strip_indent=0, lstrip=False, options=None,
     :param bool lstrip: 是否去除每一行开始的空白符
     :param dict options: 解析Markdown时的配置参数。
        PyWebIO使用 `marked <https://marked.js.org/>`_ 解析Markdown, 可配置项参见: https://marked.js.org/using_advanced#options (仅支持配置string和boolean类型的项)
+    :param bool sanitize: 是否使用 `DOMPurify <https://github.com/cure53/DOMPurify>`_ 对内容进行过滤来防止XSS攻击。
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
 
     当在函数中使用Python的三引号语法输出多行内容时,为了排版美观可能会对Markdown文本进行缩进,
@@ -396,7 +398,8 @@ def put_markdown(mdcontent, strip_indent=0, lstrip=False, options=None,
         lines = (i.lstrip() for i in mdcontent.splitlines())
         mdcontent = '\n'.join(lines)
 
-    spec = _get_output_spec('markdown', content=mdcontent, options=options, scope=scope, position=position)
+    spec = _get_output_spec('markdown', content=mdcontent, options=options, sanitize=sanitize,
+                            scope=scope, position=position)
     return Output(spec)
 
 
@@ -684,7 +687,7 @@ def put_image(src, format=None, title='', width=None, height=None,
     height = 'height="%s"' % height if height is not None else ''
 
     html = r'<img src="{src}" alt="{title}" {width} {height}/>'.format(src=src, title=title, height=height, width=width)
-    return put_html(html, scope=scope, position=position)
+    return put_html(html, sanitize=False, scope=scope, position=position)
 
 
 def put_file(name, content, label=None, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
@@ -735,7 +738,7 @@ def put_link(name, url=None, app=None, new_window=False, scope=Scope.Current,
     href = 'javascript:WebIO.openApp(%r, %d)' % (app, new_window) if app is not None else url
     target = '_blank' if (new_window and url) else '_self'
     html = '<a href="{href}" target="{target}">{name}</a>'.format(href=href, target=target, name=name)
-    return put_html(html, scope=scope, position=position)
+    return put_html(html, sanitize=False, scope=scope, position=position)
 
 
 def put_processbar(name, init=0, label=None, auto_close=False, scope=Scope.Current,
@@ -831,7 +834,7 @@ def put_loading(shape='border', color='dark', scope=Scope.Current, position=Outp
     html = """<div class="spinner-{shape} text-{color}" role="status">
                 <span class="sr-only">Loading...</span>
             </div>""".format(shape=shape, color=color)
-    return put_html(html, scope=scope, position=position)
+    return put_html(html, sanitize=False, scope=scope, position=position)
 
 
 @safely_destruct_output_when_exp('content')

+ 1 - 0
setup.py

@@ -51,6 +51,7 @@ setup(
             "html/css/codemirror.min.css",
             "html/js/FileSaver.min.js",
             "html/js/prism.min.js",
+            "html/js/purify.min.js",
             "html/js/pywebio.min.js",
             "html/js/mustache.min.js",
             "html/js/jquery.min.js",

+ 17 - 3
webiojs/src/models/output.ts

@@ -49,8 +49,15 @@ marked.setOptions({
 let Markdown = {
     handle_type: 'markdown',
     get_element: function (spec: any) {
-        // https://marked.js.org/using_advanced#options
-        return $(marked(spec.content, spec.options));
+        // spec.options, see also https://marked.js.org/using_advanced#options
+        let html_str = marked(spec.content, spec.options);
+        if (spec.sanitize)
+            try {
+                html_str = DOMPurify.sanitize(html_str);
+            } catch (e) {
+                console.log('Sanitize html failed: %s\nHTML: \n%s', e, html_str);
+            }
+        return $(html_str);
     }
 };
 
@@ -68,7 +75,14 @@ function parseHtml(html_str: string) {
 let Html = {
     handle_type: 'html',
     get_element: function (spec: any) {
-        return parseHtml(spec.content);
+        let html_str = spec.content;
+        if (spec.sanitize)
+            try {
+                html_str = DOMPurify.sanitize(html_str);
+            } catch (e) {
+                console.log('Sanitize html failed: %s\nHTML: \n%s', e, html_str);
+            }
+        return parseHtml(html_str);
     }
 };
 

+ 2 - 1
webiojs/src/vendor.d.ts

@@ -4,4 +4,5 @@ declare let saveAs: any;
 declare let CodeMirror: any;
 declare let bsCustomFileInput: any;
 declare let Toastify: any;
-declare let Prism: any;  // Prism.js
+declare let Prism: any;  // Prism.js
+declare let DOMPurify: any;  // DOMPurify.js

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff