一、前言
对于用户编辑文本的功能点,很多时候,业务上需要允许用户输入自定义的样式,比较简单直接的方案就是使用富文本:
支持用户在前端自定义的html传入,最终同样以html的形式展现。不同的业务可能有各种各样不同的具体实现形式,
但殊途同归,最终都可以抽象成“输入->持久化->输出”来表述。
如果整个过程不进行任何处理,就会产生XSS漏洞。然而常见的XSS防护方案中的编码转义,完全不适用于这种场景。
那么剩下的方案就只有把不安全的内容过滤掉了。
通常,安全人员在测试上面的流程时,会在前端提交html富文本给后端之前,对Post请求进行拦截,然后在内容中插入类似下面的代码,来证明漏洞存在:
<img src=1 οnerrοr="alert(123)">
<script>alert(123);</script>
如果应用后端没有进行过滤,查看文章时,页面会执行插入的js代码,弹窗。
当安全人员将这样的PoC提交给开发时,如果开发是一个小白,往往第一反应是用黑名单的方式过滤掉onerror事件、script标签。
然而有经验的开发同学都知道,黑名单不管如何更新维护,往往都是白白浪费精力,最终都难以逃脱被绕过的下场。最有效的方案是使用白名单过滤。
三、过滤方案及实现
对于上面的业务流程,过滤可以在两个环节实现。一个是服务端在接收到前端传入的数据后,对数据进行过滤处理,另外一个是数据返回给前端,前端将数据渲染到页面呈现之前。
jsoup
jsoup 是一款 Java 的 HTML 解析器,可直接解析某个 URL 地址、HTML 文本内容。它提供了一套非常省力的 API,可通过 DOM、CSS 以及类似于 JQuery 的操作方法来取出和操作数据。基于MIT协议发布,可放心用于商业项目。
jsoup内置了一些白名单的标签属性list,同时支持用户自定义,或者在此基础上根据需求灵活扩展。
先看一个简单的Demo。
maven依赖:
<dependency>
<!-- jsoup HTML parser library @ https://jsoup.org/ -->
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.14.3</version>
</dependency>
public static HtmlText truncateHtmlText(String htmlText, int pureTextSize) { Safelist safeList = Safelist.none(); if (properties.whiteTagWithAttrs != null && properties.whiteTagWithAttrs.size() > 0) { properties.whiteTagWithAttrs.forEach((tag, attr) -> { if (StringUtils.isBlank(attr)) { safeList.addTags(tag); } else { safeList.addAttributes(tag, attr.split(",")); } }); } String cleanHtml = Jsoup.clean(htmlText, safeList); Document doc = Jsoup.parse(cleanHtml); Element body = doc.body(); List<HtmlText> htmls = parseDocElement(body); return concatHtmlTextByPureSize(htmls, pureTextSize); }
properties中内容为:
public static Map<String, String> whiteTagWithAttrs = new HashMap<>();
如下为配置文件内容:
white-tag-with-attrs.a=href,data-params,data-type,data-coin white-tag-with-attrs.span=disablecaretend,contenteditable white-tag-with-attrs.source=src,source,type white-tag-with-attrs.mark=data-cid,data-type,data-attitude white-tag-with-attrs.img=src white-tag-with-attrs.div=id white-tag-with-attrs.p= white-tag-with-attrs.vedio=poster,type,width,height,controls
private static List<HtmlText> parseDocElement(Element element) { if (null == element) { return new ArrayList<>(); } if (element.childNodes().size() == 0) { return Lists.newArrayList(HtmlText.builder().origin(element.outerHtml()).pure(element.text()).build()); } HtmlText parentHtml = null; List<HtmlText> htmlTexts = new ArrayList<>(); for (Node node : element.childNodes()) { if (node instanceof TextNode) { TextNode textNode = (TextNode) node; boolean isBody = textNode.parent().nodeName().equals("body"); //ignore body tag if (!isBody) { //已经包含了parentHtml,直接跳出,不再处理 parentHtml = HtmlText.builder().origin(textNode.parent().outerHtml()).pure(textNode.text()).build(); break; } else { htmlTexts.add(HtmlText.builder().origin(textNode.outerHtml()).pure(textNode.text()).build()); } } else if (node instanceof Element) { Element elementNode = (Element) node; if (!elementNode.hasText()) { htmlTexts.add(HtmlText.builder().origin(elementNode.outerHtml()).pure(elementNode.text()).build()); } else { List<HtmlText> childHtmlTexts = parseDocElement(elementNode); htmlTexts.addAll(childHtmlTexts); } } } if (parentHtml != null) { parentHtml.setPure(Jsoup.clean(parentHtml.getOrigin(), Safelist.none())); return Lists.newArrayList(parentHtml); } return htmlTexts; } private static HtmlText concatHtmlTextByPureSize(List<HtmlText> htmlTexts, int pureSize) { if (null == htmlTexts || htmlTexts.size() == 0) { return null; } StringBuilder htmlBuilder = new StringBuilder(); StringBuilder pureBuilder = new StringBuilder(); AtomicInteger atomicInteger = new AtomicInteger(0); for (HtmlText html : htmlTexts) { int size = atomicInteger.addAndGet(html.getPure().length()); htmlBuilder.append(html.getOrigin().replaceAll("\n","")); pureBuilder.append(html.getPure()); if (pureSize > 0 && size >= pureSize) { break; } } return HtmlText.builder().origin(htmlBuilder.toString()).pure(pureBuilder.toString()).build(); }
HtmlText s = HtmlText.truncateHtmlText(html, 20);
public class HtmlText implements Serializable { /** * 原始文本 */ private String origin; /** * 纯文本内容 (不含任何标签) */ private String pure;
}
addTags(String… tags)方法中的参数内容,表示html标签(元素)的白名单。
addAttributes(String tag, String… attributes),表示指定的标签允许哪些属性。
addProtocols(String tag, String attribute, String… protocols),表示指定的标签里的指定属性允许使用哪些协议。
addEnforcedAttribute(String tag, String attribute, String value),表示指定的标签,不论有没有对应的属性和属性值,都强制加上。
再来看我们Demo中用到的basicWithImages()方法。在basic()的基础上又增加了img标签和一些可能用到的属性。