首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  程序员

允许用户做富文本编辑的站点要怎么防 XSS 呢

  •  
  •   ikaros · 44 天前 · 1450 次点击
    这是一个创建于 44 天前的主题,其中的信息可能已经有所发展或是发生改变。

    需求是需要用户至少能使用 a、img 标签, 有现成的后端正则过滤的表达式吗(感觉会很麻烦), 还是说前端在展示的时候做?

    16 回复  |  直到 2019-03-13 09:24:16 +08:00
        1
    cpdyj0   44 天前 via Android
    后端找个 parser,parse 一遍试试
        2
    AngryPanda   44 天前   ♥ 1
        3
    lizheming   44 天前
    比较干净的方法只能是白名单,有两种思路,一种是如 1 楼所述用 parser 解析后白名单过滤,一种是自定义一套语法然后非语法的 HTML 全部过滤掉,例如 Markdown 和 UBBCode (暴露年纪了) ....
    两种思路其实差不多。需要使用 parser 主要是因为不仅仅只是过滤标签,合法标签里的非法属性也需要过滤,例如`onload`, `onerror` 之类的会触发 JS 行为的属性,这时候用正则去做就挺麻烦了,即使写出来了也没办法维护。
        4
    Shynoob   43 天前
    参考下
    ```java
    // 预编译 XSS 过滤正则表达式
    private static List<Pattern> xssPatterns = ListUtils.newArrayList(
    Pattern.compile("(<\\s*(script|link|style|iframe)([\\s\\S]*?)(>|<\\/\\s*\\1\\s*>))|(</\\s*(script|link|style|iframe)\\s*>)", Pattern.CASE_INSENSITIVE),
    Pattern.compile("\\s*(href|src)\\s*=\\s*(\"\\s*(javascript|vbscript):[^\"]+\"|'\\s*(javascript|vbscript):[^']+'|(javascript|vbscript):[^\\s]+)\\s*(?=>)", Pattern.CASE_INSENSITIVE),
    Pattern.compile("\\s*on[a-z]+\\s*=\\s*(\"[^\"]+\"|'[^']+'|[^\\s]+)\\s*(?=>)", Pattern.CASE_INSENSITIVE),
    Pattern.compile("(eval\\((.*?)\\)|xpression\\((.*?)\\))", Pattern.CASE_INSENSITIVE)
    );

    /**
    * XSS 非法字符过滤,内容以<!--HTML-->开头的用以下规则(保留标签)
    * @author ThinkGem
    */
    public static String xssFilter(String text) {
    String oriValue = StringUtils.trim(text);
    if (text != null){
    String value = oriValue;
    for (Pattern pattern : xssPatterns) {
    Matcher matcher = pattern.matcher(value);
    if (matcher.find()) {
    value = matcher.replaceAll(StringUtils.EMPTY);
    }
    }
    // 如果开始不是 HTML,XML,JOSN 格式,则再进行 HTML 的 "、<、> 转码。
    if (!StringUtils.startsWithIgnoreCase(value, "<!--HTML-->") // HTML
    && !StringUtils.startsWithIgnoreCase(value, "<?xml ") // XML
    && !StringUtils.contains(value, "id=\"FormHtml\"") // JFlow
    && !(StringUtils.startsWith(value, "{") && StringUtils.endsWith(value, "}")) // JSON Object
    && !(StringUtils.startsWith(value, "[") && StringUtils.endsWith(value, "]")) // JSON Array
    ){
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < value.length(); i++) {
    char c = value.charAt(i);
    switch (c) {
    case '>':
    sb.append(">");
    break;
    case '<':
    sb.append("<");
    break;
    case '\'':
    sb.append("'");
    break;
    case '\"':
    sb.append(""");
    break;
    // case '&':
    // sb.append("&");
    // break;
    // case '#':
    // sb.append("#");
    // break;
    default:
    sb.append(c);
    break;
    }
    }
    value = sb.toString();
    }
    if (logger.isInfoEnabled() && !value.equals(oriValue)){
    logger.info("xssFilter: {} <=<=<= {}", value, text);
    }
    return value;
    }
    return null;
    }

    // 预编译 SQL 过滤正则表达式
    private static Pattern sqlPattern = Pattern.compile("(?:')|(?:--)|(/\\*(?:.|[\\n\\r])*?\\*/)|(\\b(select|update|and|or|delete|insert|trancate|char|into|substr|ascii|declare|exec|count|master|into|drop|execute)\\b)", Pattern.CASE_INSENSITIVE);

    /**
    * SQL 过滤,防止注入,传入参数输入有 select 相关代码,替换空。
    * @author ThinkGem
    */
    public static String sqlFilter(String text){
    if (text != null){
    String value = text;
    Matcher matcher = sqlPattern.matcher(text);
    if (matcher.find()) {
    value = matcher.replaceAll(StringUtils.EMPTY);
    }
    if (logger.isWarnEnabled() && !value.equals(text)){
    logger.info("sqlFilter: {} <=<=<= {}", value, text);
    return StringUtils.EMPTY;
    }
    return value;
    }
    return null;
    }
    ```
        5
    lastpass   43 天前 via Android
    emmm,你别直接使用 html 模式不行吗?
        6
    wusatosi   43 天前
    用<noscript>包一下就好了吧
        7
    mytry   43 天前   ♥ 1
    在后端正则过滤基本会出 XSS,用三方库 parser 的也容易出 XSS。

    最稳妥的方案,在前端渲染前用浏览器内置的 DOMParser API 把字符串转成 DOM 树,只保留白名单的标签和属性,然后再添加到文档里。
        8
    corningsun   43 天前
    XSS 还是得后端处理的,建议直接保存原文到数据库,查询返回前端前做一次转化,后端有很多现成的库可以参考的。

    比如说:Jsoup 有很多预定义好的白名单供选择


    public static Whitelist relaxed() {
    return new Whitelist()
    .addTags(
    "a", "b", "blockquote", "br", "caption", "cite", "code", "col",
    "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6",
    "i", "img", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong",
    "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u",
    "ul")

    .addAttributes("a", "href", "title")
    .addAttributes("blockquote", "cite")
    .addAttributes("col", "span", "width")
    .addAttributes("colgroup", "span", "width")
    .addAttributes("img", "align", "alt", "height", "src", "title", "width")
    .addAttributes("ol", "start", "type")
    .addAttributes("q", "cite")
    .addAttributes("table", "summary", "width")
    .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width")
    .addAttributes(
    "th", "abbr", "axis", "colspan", "rowspan", "scope",
    "width")
    .addAttributes("ul", "type")

    .addProtocols("a", "href", "ftp", "http", "https", "mailto")
    .addProtocols("blockquote", "cite", "http", "https")
    .addProtocols("cite", "cite", "http", "https")
    .addProtocols("img", "src", "http", "https")
    .addProtocols("q", "cite", "http", "https")
    ;
    }


    代码调用示例:

    import org.apache.commons.lang3.StringUtils;
    import org.jsoup.Jsoup;
    import org.jsoup.nodes.Document;
    import org.jsoup.safety.Whitelist;

    public class JsoupUtil {

    private static final Whitelist WHITELIST = Whitelist.relaxed();

    static {
    // 富文本编辑时一些样式是使用 style 来进行实现的
    // 比如红色字体 style="color:red;"
    // 所以需要给所有标签添加 style 属性
    WHITELIST.addAttributes(":all", "style");
    WHITELIST.addAttributes(":all", "class");
    WHITELIST.addAttributes(":all", "target");
    WHITELIST.addAttributes(":all", "spellcheck");
    }

    private JsoupUtil() {
    }

    private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);

    public static String clean(String content) {
    if (StringUtils.isBlank(content)) {
    return "";
    }
    return Jsoup.clean(content, "", WHITELIST, OUTPUT_SETTINGS);
    }

    }
        9
    mytry   43 天前
    @mytry 几年前写过个演示的,可以参考下~ https://www.etherdream.com/FunnyScript/richtext_safe_render.html
        10
    wktrf   43 天前 via Android
    我记得贴吧的是直接将标签转换了,例如 img 标签转换成[image]baidu.com[image]了, 不过听说图片内也可以藏脚本,所以上传的图片还得处理
        11
    krixaar   43 天前
    让用户用所见即所得编辑器写 Markdown 或者用 BBcode ?
        12
    111111111111   43 天前 via Android
    UBB 代码了解一下?
        13
    oldmyth   43 天前
    建议直接百度
    www.baidu.com
        14
    ikaros   43 天前
    @lizheming 对,最后思考了一下可能还是用 markdown 比较好
    @cpdyj0 这种方式感觉还是有点问题,主要是#3 说的,有些属性什么的也要禁用
    @Shynoob 只是禁用标签还是不够的
    @lastpass 当前在用的一个富文本编辑器提交上来就是 html, 一时脑子抽没想起还有 markdown 这种
        15
    cpdyj0   43 天前
    @ikaros 是的,parse 一遍的话首先要保证 parser 尽量没有严重的 bug,然后就是白名单了,不顺眼的东西都不能出现
        16
    laozhoubuluo   43 天前
    如果是新站点,建议还是 UBB 代码或者 Markdown 实现,因为 html 乱七八糟的属性太多,很容易过滤不到......
    如果已经是 html 了,那就认命吧......
    Discuz! X 门户就是 HTML,门户出了 XSS 没法无伤修复......
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   4234 人在线   最高记录 5043   ·  
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.3 · 19ms · UTC 02:12 · PVG 10:12 · LAX 19:12 · JFK 22:12
    ♥ Do have faith in what you're doing.
    沪ICP备16043287号-1