浅谈跨站脚本攻击(XSS)那些事儿

2 分钟阅读

随着互联网的高速发展,各种网络黑产的技术使用门槛也在逐步降低,这些年来,针对用户的个人信息被盗用,密码泄露等网络安全问题层出不穷,随着问题的暴露,人们对个人信息的安全问题愈发的重视。

开发者不仅要面对传统的XSS,CSRF攻击手段,还要面对网络劫持,中间人攻击,非法调用API等等各种新型的攻击方式挑战。

本文就浅谈Java服务端中XSS相关的那点事情。

什么是XSS?

跨站脚本攻击(Cross-site scripting,XSS)是一种安全漏洞,攻击者可以利用这种漏洞在网站上注入恶意的客户端代码。若受害者运行这些恶意代码,攻击者就可以突破网站的访问限制并冒充受害者。根据开放式 Web 应用安全项目(OWASP),XSS在2017年被认为7种最常见的Web应用程序漏洞之一。

如果Web应用程序没有部署足够的安全验证,那么,这些攻击很容易成功。浏览器无法探测到这些恶意脚本是不可信的,所以,这些脚本可以任意读取 cookie,session tokens,或者其它敏感的网站信息,或者让恶意脚本重写HTML内容。

XSS 攻击可以分为 3 类:存储型(持久型)、反射型(非持久型)、DOM 型。

存储型 XSS

一般是指注入恶意内容,且服务器由于没有过滤或拦截,导致恶意内容存储到数据库。再经由其他用户访问下发给其他访问的用户执行。例如:论天的聊天,微博的回复等等。存储型也是危害影响最严重的一种XSS

反射型 XSS

应用程序或 API 包括未经验证和未经转义的用户输入作为 HTML 输出的一部分。成功的攻击可以让攻击者在受害者的浏览器中执行任意 HTML 和 JavaScript。通常,用户需要与一些指向攻击者控制页面的恶意链接进行交互,例如论坛、微博评论,访问日志等。

基于 DOM 的 XSS

通过劫持或注入等手段修改原始的客户端代码(HTML, JavaScript, CSS, URL),页面本身可能并没有变化,实际一串包含恶意的代码已经被意外执行。 相对来说,最难避免,同时也是危害相对较轻的一种XSS

XSS检测

1
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e

更多更详细的使用这段 恶意代码 的攻击策略和演示,请查看 Unleashing-an-Ultimate-XSS-Polyglot

XSS预防

反射型XSS和存储型XSS的预防

主要是输入数据校验HTML属性,HTML正文, Javascript,CSS,URL上下文的输出编码, 避免将不可信数据当作代码下发给浏览器,被浏览器错误的执行。

详细的XSS防护规则总结 OWASP

基于DOM的XSS预防

主要是前端的一些防御措施

  1. 避免直接用 .innerHTML, .outerHTML, .write, .innerHTML, 使用 .innerText, .textContentelement.setAttribute()(对部分属性(e.g. width, height, ref .etc)是安全的,对href,onclick等属性也是不安全的)
  2. 避免将不可信数据拼接字符串传入给那些能将字符串当代码执行的API。e.g. onlick, onload,eval(), setTimeout(), setInterval(), href.etc
  3. 避免不可信数据对对象属性的访问, 仅将不可信数据当作右值使用。例如:object[x]
  4. 避免直接使用eval()将json转换为javascript对象。使用JSON.toJSON()和JSON.paree()
  5. 使用安全沙箱执行JavaScript脚本

目前主流的前端框架都针对XSS攻击有相关的防护和最佳实践文档, 例如:Vue.js 3.x, Angular JS

Java服务端相关

在服务端方面,更多是针对反射型和存储型XSS的防护,主要由以下几个方面的内容。

不可信数据的HTML编码

使用编码库直接将不可信数据(e.g. url参数名/参数值,header,attributes .etc)进行编码检查。可以使用现有的一些第三方编码库Apache Commons Text, Apache Commons Lang3 StringEscapeUtils工具或者OWASP Java 编码器 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//  使用common-lang3  3.7
class XSSRequestWrapper extends HttpServletRequestWrapper {

    private Map<String, String[]> sanitizedQueryString;

    public XSSRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getParameter(String name) {
        String parameter = null;
        String[] vals = getParameterMap().get(name);
        if (vals != null && vals.length > 0) {
            parameter = vals[0];
        }
        return parameter;
    }

    @Override
    public String[] getParameterValues(String name) {
        return getParameterMap().get(name);
    }

    @Override
    public Enumeration<String> getParameterNames() {
        return Collections.enumeration(getParameterMap().keySet());
    }

    @Override
    public Object getAttribute(String name) {
        Object value = super.getAttribute(name);
        if (value instanceof String) {
            return StringEscapeUtils.escapeHtml4((String) value);
        }
        return value;
    }

    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        return StringUtils.isBlank(value) ? value : StringEscapeUtils.escapeHtml4(value);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        if (sanitizedQueryString == null) {
            Map<String, String[]> originalQueryString = super.getParameterMap();
            Map<String, String[]> res = new HashMap<>();
            if (originalQueryString != null) {
                for (Map.Entry<String, String[]> entry : originalQueryString.entrySet()) {
                    String[] values = null;
                    if (entry.getValue().length == 1) {
                        values = new String[]{StringEscapeUtils.escapeHtml4(entry.getValue()[0])};
                    } else {
                        values = Arrays.stream(entry.getValue()).map(StringEscapeUtils::escapeHtml4).toArray(String[]::new);
                    }
                    res.put(StringEscapeUtils.escapeHtml4(entry.getKey()), values);
                }
            }
            sanitizedQueryString = res;
        }
        return sanitizedQueryString;
    }
}

启用内容安全策略(CSP)

Content-Security-Policy是现代浏览器用来增强文档(或网页)安全性的 HTTP 响应标头的名称。Content-Security-Policy 标头允许您限制资源(如 JavaScript、CSS 或浏览器加载的几乎任何内容)的方式。

实现Java Servlet的Filter给每个请求的Header追加CSP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ContentSecurityPolicyFilter implements Filter {

    private static final String DEFAULT_CSP = "default-src 'self' data: 'unsafe-inline' 'unsafe-eval';" +
            " frame-src 'none'; " +
            " object-src 'none'; " +
            " report-uri /status;";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (response instanceof HttpServletResponse) {
            String csp = ((HttpServletResponse) response).getHeader(HttpHeaderNames.CONTENT_SECURITY_POLICY.toString());
            if (StringUtils.isBlank(csp)) {
                ((HttpServletResponse) response).setHeader(HttpHeaderNames.CONTENT_SECURITY_POLICY.toString(), DEFAULT_CSP);
            }
        }
        chain.doFilter(request, response);
    }
}

Google 提供的免费的在线CSP评估工具,针对你的CSP设置进行安全校验,建议和评估。CSP Evaluator

JEPS

反射型XSS一般都是盗取用户的Cookie,并伪造用户登录从而欺骗服务器窃取数据。现代浏览器基本上都支持设置Cookie的Http-Only属性,可以避免客户端访问cookie数据。启用Http-Only和Secure示例:

1
Set-Cookie: Secure; HttpOnly

实现Java Servlet的Filter给响应设置Cookie配置。

1
2
3
4
5
6
7
8
9
10
11
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    if (response instanceof HttpServletResponse hsr) {
        //  set-cookie: Secure; HttpOnly
        String cookie = hsr.getHeader(HttpHeaderNames.SET_COOKIE.toString());
        if (!cookie.contains(DEFAULT_COOKIE)) {
            hsr.setHeader(HttpHeaderNames.SET_COOKIE.toString(), cookie + DEFAULT_COOKIE);
        }
    }
    chain.doFilter(request, response);
}

也可以使用web.xml配置

1
2
3
4
5
6
<session-config>
    <cookie-config>
        <http-only>true</http-only>
        <secure>true</secure>
    </cookie-config>
</session-config>

更详细的信息查看MDN HTTP Cookie

不暴露非必要的服务

针对攻击,最好的手段是物理隔绝,不和你玩了,免疫你全部攻击。在设计和架构上对于没有必要开放给用户的页面和端口等,可以使用服务剥离,防火墙等手段将风险隔绝在门外。

引用参考

  1. OWASP XSS
  2. OWASP DOM XSS

知识共享许可协议

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 TinyZ Zzh (包含链接: https://tinyzzh.github.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。 如有任何疑问,请 与我联系 (tinyzzh815@gmail.com)

TinyZ Zzh

TinyZ Zzh

专注于高并发服务器、网络游戏相关(Java、PHP、Unity3D、Unreal Engine等)技术,热爱游戏事业, 正在努力实现自我价值当中。

评论

  点击开始评论...