XSS是跨站脚本攻击(Cross Site Scripting),为不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。

XSS 简介

举一个简单的例子,就是留言板。我们知道留言板通常的任务就是把用户留言的内容展示出来。正常情况下,用户的留言都是正常的语言文字,留言板显示的内容也就没毛病。然而这个时候如果有人不按套路出牌,在留言内容中丢进去一行

1
<script>alert("aaa")</script>

那么留言板界面的网页代码就会变成形如以下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<html>
    <head>
       <title>Board</title>
    </head>
    <body>
    <div id="board">
        <script>alert("aaa")</script>
    </div>     
    </body>
</html>

那么这个时候问题就来了,当浏览器解析到用户输入的代码那一行时会发生什么呢?答案很显然,浏览器并不知道这些代码改变了原本程序的意图,会照做弹出一个信息框。

既然能够执行脚本,那么,这些脚本完全可以是:

  • 链接劫持
1
<script>window.location.href="http://www.baidu.com";</script>
  • 盗取cookie
1
<script>alert("document.cookie");</script>

对于攻击者来说,能够让受害者浏览器执行恶意代码的唯一方式,就是把代码注入到受害者从网站下载的网页中, 这就是xss攻击。

XSS 攻击类型

通常XSS攻击分为:反射型xss攻击, 存储型xss攻击DOM型xss攻击。同时注意以下例子只是简单的向你解释这三种类型的攻击方式而已,实际情况比这个复杂,具体可以再结合最后一节深入理解。

反射型xss攻击

反射型的攻击需要用户主动的去访问带攻击的链接,攻击者可以通过邮件或者短信的形式,诱导受害者点开链接。如果攻击者配合短链接URL,攻击成功的概率会更高。

在一个反射型XSS攻击中,恶意文本属于受害者发送给网站的请求中的一部分。随后网站又把恶意文本包含进用于响应用户的返回页面中,发还给用户。

img

1、用户误点开了带攻击的url : http://xxx?name=<script>alert('aaa')</script>

2、网站给受害者的返回中正常的网页

3、用户的浏览器收到文本后执行页面合法脚本,这时候页面恶意脚本会被执行,会在网页中弹窗aaa

这种攻击方式发生在我们合法的js执行中,服务器无法检测我们的请求是否有攻击的危险

存储型xss攻击

这种攻击方式恶意代码会被存储在数据库中,其他用户在正常访问的情况下,也有会被攻击,影响的范围比较大。

img

1、攻击者通过评论表单提交将<script>alert(‘aaa’)</script>提交到网站

2、网站后端对提交的评论数据不做任何操作,直接存储到数据库中

3、其他用户访问正常访问网站,并且需要请求网站的评论数据

4、网站后端会从数据库中取出数据,直接返回给用户

5、用户得到页面后,直接运行攻击者提交的代码<script>alert(‘aaa’)</script>,所有用户都会在网页中弹出aaa的弹窗

DOM型xss攻击

基于DOM的XSS攻击是反射型攻击的变种。服务器返回的页面是正常的,只是我们在页面执行js的过程中,会把攻击代码植入到页面中。

img

1、用户误点开了带攻击的url : http://xxx?name=<script>alert('aaa')</script>

2、网站给受害者的返回中正常的网页

3、用户的浏览器收到文本后执行页面合法脚本,这时候页面恶意脚本会被执行,会在网页中弹窗aaa

这种攻击方式发生在我们合法的js执行中,服务器无法检测我们的请求是否有攻击的危险

XSS 攻击的危害

  • 通过document.cookie盗取cookie
  • 使用js或css破坏页面正常的结构与样式
  • 流量劫持(通过访问某段具有window.location.href定位到其他页面)
  • Dos攻击:利用合理的客户端请求来占用过多的服务器资源,从而使合法用户无法得到服务器响应。
  • 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作。
  • 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
  • DOS(拒绝服务)客户端浏览器。
  • 钓鱼攻击,高级的钓鱼技巧。
  • 劫持用户Web行为,甚至进一步渗透内网。
  • 蠕虫式挂马攻击、刷广告、刷浏量、破坏网上数据

通过xss盗用cookie危害是什么?

csrf攻击其实是不能盗用cookie的,它只是以当前的名义进行恶意操作;而xss攻击是可以直接盗用cookie。

那盗用cookie的危害是什么?比如拿到用户的cookie信息,然后传送到攻击者自己的服务器,从cookie中提取敏感信息,拿到用户的登录信息,或者攻击者可以通过修改DOM在页面上插入一个假的登陆框,也可以把表单的action属性指向他自己的服务器地址,然后欺骗用户提交自己的敏感信息。

这就是为什么cookie也是要防御的

XSS 攻击的防御

XSS攻击其实就是代码的注入。用户的输入被编译成恶意的程序代码。所以,为了防范这一类代码的注入,需要确保用户输入的安全性。对于攻击验证,我们可以采用以下两种措施:

  • 编码,就是转义用户的输入,把用户的输入解读为数据而不是代码
  • 校验,对用户的输入及请求都进行过滤检查,如对特殊字符进行过滤,设置输入域的匹配规则等

具体比如:

  • 对于验证输入,我们既可以在服务端验证,也可以在客户端验证
  • 对于持久性和反射型攻击服务端验证是必须的,服务端支持的任何语言都能够做到
  • 对于基于DOM的XSS攻击,验证输入在客户端必须执行,因为从服务端来说,所有发出的页面内容是正常的,只是在客户端js代码执行的过程中才发生可攻击
  • 但是对于各种攻击方式,我们最好做到客户端和服务端都进行处理

其它还有一些辅助措施,比如:

  • 入参长度限制: 通过以上的案例我们不难发现xss攻击要能达成往往需要较长的字符串,因此对于一些可以预期的输入可以通过限制长度强制截断来进行防御。
  • 设置cookie http-only为true

验证输入

自定义@Xss注解,将定义的Xss注解放在字段或者方法的上方

@Xss注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * 自定义xss校验注解
 * 
 * @author 7bin
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER })
@Constraint(validatedBy = { XssValidator.class })
    public @interface Xss
{
    String message()

    default "不允许任何脚本运行";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

自定义校验器XssValidator

 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
/**
 * 自定义xss校验注解实现
 * 
 * @author 7bin
 */
public class XssValidator implements ConstraintValidator<Xss, String>
{
    private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />";

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext)
    {
        if (StringUtils.isBlank(value))
        {
            return true;
        }
        return !containsHtml(value);
    }

    public static boolean containsHtml(String value)
    {
        Pattern pattern = Pattern.compile(HTML_PATTERN);
        Matcher matcher = pattern.matcher(value);
        return matcher.matches();
    }
}

使用自定义Xss注解

实体类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class SysUser extends BaseEntity
{
....
    @Xss(message = "用户账号不能包含脚本字符")
    @NotBlank(message = "用户账号不能为空")
    @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符")
    public String getUserName() {
        return userName;
    }
}

Controller

在Controller的方法上,给参数SysUser类加上@Validated来修饰,这样校验就可以生效了

1
2
3
4
5
@PutMapping
public ApiResponse edit(@Validated @RequestBody SysUser user)
{
    ...
}

escapeHTML

在服务端添加XSS的Filter,用于正确处理转义字符,产生正确的Java、JavaScript、HTML、XML和SQL代码

FilterConfig:注册XSS过滤器

 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
@Configuration
public class FilterConfig
{
    @Value("${xss.excludes}")
    private String excludes;

    @Value("${xss.urlPatterns}")
    private String urlPatterns;

    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    @ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
    public FilterRegistrationBean xssFilterRegistration()
    {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setDispatcherTypes(DispatcherType.REQUEST);
        registration.setFilter(new XssFilter());
        registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
        registration.setName("xssFilter");
        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
        Map<String, String> initParameters = new HashMap<String, String>();
        initParameters.put("excludes", excludes);
        registration.setInitParameters(initParameters);
        return registration;
    }

}

XssFilter:使用自定义XssHttpServletRequestWrapper替换默认的request

 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
/**
 * 防止XSS攻击的过滤器
 * 
 * @author 7bin
 */
public class XssFilter implements Filter
{
    /**
     * 排除链接
     */
    public List<String> excludes = new ArrayList<>();

    @Override
    public void init(FilterConfig filterConfig) throws ServletException
    {
        String tempExcludes = filterConfig.getInitParameter("excludes");
        if (StringUtils.isNotEmpty(tempExcludes))
        {
            String[] url = tempExcludes.split(",");
            for (int i = 0; url != null && i < url.length; i++)
            {
                excludes.add(url[i]);
            }
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException
    {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        if (handleExcludeURL(req, resp))
        {
            chain.doFilter(request, response);
            return;
        }
        XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
        chain.doFilter(xssRequest, response);
    }

    private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response)
    {
        String url = request.getServletPath();
        String method = request.getMethod();
        // GET DELETE 不过滤
        if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method))
        {
            return true;
        }
        return StringUtils.matches(url, excludes);
    }

    @Override
    public void destroy()
    {

    }
}

XssHttpServletRequestWrapper

xss过滤(清除所有HTML标签,但是不删除标签内的内容):json = EscapeUtil.clean(json).trim();

 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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/**
 * XSS过滤处理
 * 
 * @author 7bin
 */
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper
{
    /**
     * @param request
     */
    public XssHttpServletRequestWrapper(HttpServletRequest request)
    {
        super(request);
    }

    @Override
    public String[] getParameterValues(String name)
    {
        String[] values = super.getParameterValues(name);
        if (values != null)
        {
            int length = values.length;
            String[] escapesValues = new String[length];
            for (int i = 0; i < length; i++)
            {
                // 防xss攻击和过滤前后空格
                escapesValues[i] = EscapeUtil.clean(values[i]).trim();
            }
            return escapesValues;
        }
        return super.getParameterValues(name);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException
    {
        // 非json类型,直接返回
        if (!isJsonRequest())
        {
            return super.getInputStream();
        }

        // 为空,直接返回
        String json = IOUtils.toString(super.getInputStream(), "utf-8");
        if (StringUtils.isEmpty(json))
        {
            return super.getInputStream();
        }

        // xss过滤
        json = EscapeUtil.clean(json).trim();
        byte[] jsonBytes = json.getBytes("utf-8");
        final ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes);
        return new ServletInputStream()
        {
            @Override
            public boolean isFinished()
            {
                return true;
            }

            @Override
            public boolean isReady()
            {
                return true;
            }

            @Override
            public int available() throws IOException
            {
                return jsonBytes.length;
            }

            @Override
            public void setReadListener(ReadListener readListener)
            {
            }

            @Override
            public int read() throws IOException
            {
                return bis.read();
            }
        };
    }

    /**
     * 是否是Json请求
     * 
     */
    public boolean isJsonRequest()
    {
        String header = super.getHeader(HttpHeaders.CONTENT_TYPE);
        return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
    }
}

http-only

如果某一个Cookie 选项被设置成 HttpOnly = true 的话,那此Cookie 只能通过服务器端修改,JS 是操作不了的,对于 document.cookie 来说是透明的。

http-only 的应用场景是防止 XSS 攻击。

举例:

例如我在评论区写了一段 hack JS,服务端因存在 XSS 漏洞,没有对 < 进行转义。导致这段 JS 在其他人打开此页面的时候也被执行了。

如果我上面的 hack JS 中写了获取所有 cookie,并发送到我的服务器上,这样用户的登录信息就泄漏了。

如果 cookie 开启了 http-only 之后,我的hack js 无法获取到用户的 cookie,它们的登录信息就无法泄漏了。

以 Google 翻译为例子,初次打开时,Cookie里面是这样的一共有4条记录,注意第二个最右侧倒数第三个字段有一个√, 这个对勾表明这条记录是 HttpOnly = true 的,对于Js,你是拿不到的。

image-20230614194452836

服务端设置Cookie

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@RequestMapping("/login")
@ResponseBody
public void login(HttpServletRequest request, HttpServletResponse response) throws IOException {
    Cookie cookie = new Cookie("access_token", UUID.randomUUID().toString());
    cookie.setHttpOnly(true); // 这里
    cookie.setPath("/");
    cookie.setDomain("localhost");
    response.addCookie(cookie);
    response.sendRedirect("http://localhost:8088/index.html");
}

xss攻击和csrf攻击配合

一般攻击可能不是单一的行为,而是可能会组合攻击;比如xss攻击一般还可以配合csrf攻击进行配合攻击,这里给个例子,方便你理解;注意,只是仅仅方便你理解,实际不是这么简单。

假设你可以通过如下GET请求方式进行修改密码,这是典型的csrf攻击方式:开发安全 - CSRF 详解

1
2
http://127.0.0.1/test/vulnerabilities/csrf/?
password_new=123456&password_conf=123456&Change=Change

那么你可以通过如下方式xss攻击添加脚本

1
2
<script type="text/javascript" src="http://127.0.0.1/test/vulnerabilities/csrf/?
password_new=123456&password_conf=123456&Change=Change#"></script>

参考链接

开发安全 - XSS 详解