XSS漏洞以及jsoup的使用


一、前言
对于用户编辑文本的功能点,很多时候,业务上需要允许用户输入自定义的样式,比较简单直接的方案就是使用富文本:
支持用户在前端自定义的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标签和一些可能用到的属性。
 


版权声明:本文为zpflwy1314原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。