防御Xss攻击的几种方法

防御Xss攻击的几种方法

关于xss是什么,以及它带来的危害,这里不多赘述

宁杀错,不放过。对用户输入的内容进行HTML编码

使用Spring提供的工具类 org.springframework.web.util.HtmlUtils

String result = HtmlUtils.htmlEscape("<script>alert('springboot中文社区');</script>");
System.out.println(result);
// &lt;script&gt;alert(&#39;springboot中文社区&#39;);&lt;/script&gt;

很简单粗暴,直接对所有内容进行Html编码。
编码的时机可以选择,在存储到数据库的时候编码。或者模版引擎在渲染页面的时候进行编码。
HtmlUtils 还有很多有用的静态方法,可以通过阅读源码或者文档学习

不能妄杀忠良。使用Jsoup过滤非法的HTML

有时候也不能把用户输入的所有html都给编码了。典型的场景就是 富文本编辑器。需要完整的保留富文本编辑器生成的html标签以及样式。但是又必须要防止坏人插入恶意代码

jsoup

一款html解析器,也可以用来防止xss。它可以通过配置来设定白名单的html代码。通俗的说,就是,可以设置,哪些标签,属性是系统允许的。然后通过clean方法来清除那些不符合白名单要求的标签,属性。

<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.12.1</version>
</dependency>

我封装了一个工具类

通过classpath下的一个json文件来描述Html白名单

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Whitelist;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;


public class JsoupUtils {
	
    private static final Whitelist NONE_WHITELIST = Whitelist.none();

    private static final Logger LOGGER = LoggerFactory.getLogger(JsoupUtils.class);

    // 过滤配置
    private static final Document.OutputSettings outputsettings = new Document.OutputSettings();

    /**
     * 过滤XSS字符
     * @param content
     * @return
     */
    public static String clean(String content) {
        return content == null ? null : Jsoup.clean(content, "", NONE_WHITELIST, outputsettings);
    }

    static {
    	//过滤配置参数
    	outputsettings.prettyPrint(false);
    	outputsettings.charset(StandardCharsets.UTF_8);
    	
    	//读取配置JSON文件
        Resource resource = new ClassPathResource("allow-html.json");
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))){
            StringBuilder stringBuilder = new StringBuilder();
            String line = null;
            while ((line = bufferedReader.readLine()) != null){
                stringBuilder.append(line.trim());
            }

            JSONObject jsonObject = JSON.parseObject(stringBuilder.toString());

            //允许标签
            JSONArray tags = jsonObject.getJSONArray("allow_tags");
            NONE_WHITELIST.addTags(tags.toArray(new String[tags.size()]));
            LOGGER.debug("允许标签:{}", tags);

            //允许属性
            JSONArray properties = jsonObject.getJSONArray("allow_properties");
            NONE_WHITELIST.addAttributes(":all",properties.toArray(new String[properties.size()]));
            LOGGER.debug("允许属性:{}",properties);

            //允许特殊属性
            JSONObject specialProperties = jsonObject.getJSONObject("special_properties");
            specialProperties.keySet().stream().forEach(tag -> {
                JSONArray attributes = specialProperties.getJSONArray(tag);
                NONE_WHITELIST.addAttributes(tag,attributes.toArray(new String[attributes.size()]));
                LOGGER.debug("允许特殊属性:标签={},属性={}",tag,attributes);
            });

            //允许特殊协议
            JSONObject protocols = jsonObject.getJSONObject("protocols");
            protocols.keySet().stream().forEach(tag -> {
                JSONObject protoObject = protocols.getJSONObject(tag);
                protoObject.keySet().stream().forEach(attr -> {
                    JSONArray protocolValues = protoObject.getJSONArray(attr);
                    NONE_WHITELIST.addProtocols(tag,attr,protocolValues.toArray(new String[protocolValues.size()]));
                    LOGGER.debug("允许特殊协议:标签={},属性={},协议={}",tag,attr,protocolValues);
                });
            });
            
            //固定属性值,非必须的
            JSONObject fixedProperties = jsonObject.getJSONObject("fixed_properties");
            if(fixedProperties != null && !fixedProperties.isEmpty()) {
            	fixedProperties.keySet().stream().forEach(tag -> {
            		JSONObject property = fixedProperties.getJSONObject(tag);
            		if(property != null && !property.isEmpty()) {
            			property.keySet().stream().forEach(attr -> {
            				String value = property.getString(attr);
            				NONE_WHITELIST.addEnforcedAttribute(tag, attr, value);
            				LOGGER.debug("强制属性:标签={},属性={},值={}",tag,attr,value);
            			});
            		}
                 });
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("加载XSS过滤白名单异常,请检查文件 allow-html.json");
        }
    }
}

allow-html.json

{	
    "allow_tags": [
        "a","abbr","acronym","address","area","article","aside","audio",
        "b","bdi","big","blockquote","br",
        "caption","cite","code","col","colgroup",
        "datalist","dd","del","details","div","dl","dt",
        "em",
        "fieldset","figcaption","figure","footer",
        "h1","h2","h3","h4","h5","h6","hr",
        "i","img","li","ins",
        "ol",
        "p","pre",
        "q",
        "ul",
        "small","span"
    ],
    "allow_properties":[
        "style","title"
    ],
    "special_properties": {
        "a":["href"],
        "img":["src"]
    },
    "protocols": {
        "a": {
            "href":["#","http","https","ftp","mailto"]
        },
        "blockquote":{
        	"cite":["http","https"]
        },
        "cite":{
        	"cite":["http","https"]
        },
        "q":{
        	"cite":["http","https"]
        }
    },
    "fixed_properties":{
    	"tag":{
    		"attr":"value"
    	}
    },
    "desc":{
        "allow_tags":"仅仅允许使用的标签",
        "allow_properties":"所有标签都允许使用的属性",
        "special_properties":"特殊标签允许使用的特殊属性",
        "protocols":"特殊标签,特殊属性,仅仅允许使用指定的协议",
        "fixed":"特殊标签,必须会添加的固定属性与值(会覆盖原来的)"
    }
}

演示:清除非法属性

String content = "Hello <span onclick='alert('Hello');'>KevinBlandy</span>";
content = JsoupUtils.clean(content);
System.out.println(content);
// Hello <span>KevinBlandy</span>

最后的防线。浏览器CSP策略

如果很不幸,系统被攻破,已经被注入了XSS代码。那么浏览器的CSP策略将是保证系统安全的最后一道防线

参考我以前的帖子

最后。一些建议

  1. 永远也别相信用户的输入
  2. 一般采用cookie作为凭证,那么必须要设置httpOnly=true。禁止js代码读取cookie