目录
- 什么是XSS脚本攻击
- XSS攻击的本质
- XSS 攻击分类
- 存储型 XSS 的攻击步骤:
- 反射型 XSS 的攻击步骤:
- DOM 型 XSS 的攻击步骤:
- 前端处理
- 后端处理
- 参考资料
随着互联网的高速发展
信息安全问题已经成为企业最为关注的焦点之一,而前端又是引发企业安全问题的高危据点。
在移动互联网时代,前端人员除了传统的 XSS、CSRF 等安全问题之外,又时常遭遇网络劫持、非法调用 Hybrid API 等新型安全问题。
当然,浏览器自身也在不断在进化和发展,不断引入 CSP、Same-Site Cookies 等新技术来增强安全性,但是仍存在很多潜在的威胁,这需要前端技术人员不断进行“查漏补缺”。
近几年,随着业务高速发展,前端随之面临很多安全挑战,因此积累了大量的实践经验。
我们梳理了常见的前端安全问题以及对应的解决方案,将会做成一个系列,希望可以帮助前端人员在日常开发中不断预防和修复安全漏洞
什么是XSS脚本攻击
- Cross-Site Scripting(跨站脚本攻击)简称 XSS,为了和 CSS 区分,这里把攻击的第一个字母改成了 X,于是叫做 XSS,是一种代码注入攻击。
- 攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。
- 利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。
XSS攻击的本质
- 恶意代码未经过滤,与网站正常的代码混在一起;
- 浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。
- 而由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,或者利用这些信息冒充用户向网站发起攻击者定义的请求
XSS 攻击分类
据攻击的来源,XSS 攻击可分为存储型、反射型和 DOM 型三种
类型 | 存储区 | 插入点 |
---|---|---|
存储型 | 后端数据库 | HTML |
反射型 | URL | HTML |
DOM 型 | 后端数据库/前端存储/URL | 前端 JavaScript |
存储型 XSS 的攻击步骤:
① 攻击者将恶意代码提交到目标网站的数据库中。
② 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
③ 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
④ 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
反射型 XSS 的攻击步骤:
① 攻击者构造出特殊的 URL,其中包含恶意代码。
② 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
③ 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
④ 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
反射型 XSS 与 存储型 XSS 的区别是:
存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。
由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。
DOM 型 XSS 的攻击步骤:
① 攻击者构造出特殊的 URL,其中包含恶意代码。
② 用户打开带有恶意代码的 URL。
③ 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
④ 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
DOM 型 XSS 与 前两种 XSS 的区别:
DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。
前端处理
后端处理
在微服务架构中,在网关处增加一个全局过滤器,该过滤器会将请求中提交的文本中与xss攻击相关的敏感字符进行删除
- 工具类
import lombok.extern.slf4j.Slf4j;import java.util.regex.Pattern;/*** @author samson bruce*/
@Slf4j
public class XssCleanRuleUtils {private static final Pattern[] scriptPatterns = {Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)};/*** GET请求参数过滤** @param value* @return String*/public static String xssGetClean(String value) {//过滤xss字符集return xssClean(value);}/*** post请求参数过滤** @param value value* @return String*/public static String xssPostClean(String value) {//过滤xss字符集return xssClean(value);}private static String xssClean(String value) {if (value != null) {value = value.replaceAll("\0|\n|\r", "");for (Pattern pattern : scriptPatterns) {value = pattern.matcher(value).replaceAll("");}value = value.replaceAll("<", "<").replaceAll(">", ">");}return value;}}
- 全局过滤器
import io.netty.buffer.ByteBufAllocator;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Optional;/*** @author samson bruce、yuqi li* @since 2023-07-01*/
@Slf4j
@Component
public class XssFilter implements GlobalFilter, Ordered {@SneakyThrows(Exception.class)@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// grab configuration from Config objectlog.info("----自定义防XSS攻击网关全局过滤器生效----");ServerHttpRequest serverHttpRequest = exchange.getRequest();HttpMethod method = serverHttpRequest.getMethod();String contentType = serverHttpRequest.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);URI uri = exchange.getRequest().getURI();boolean postFlag = (method == HttpMethod.POST || method == HttpMethod.PUT) &&(MediaType.APPLICATION_FORM_URLENCODED_VALUE.equalsIgnoreCase(contentType) || MediaType.APPLICATION_JSON_VALUE.equals(contentType));//过滤get请求if (method == HttpMethod.GET) {String rawQuery = uri.getRawQuery();if (StringUtils.isBlank(rawQuery)) {return chain.filter(exchange);}log.info("原请求参数为:{}", rawQuery);// 执行XSS清理rawQuery = XssCleanRuleUtils.xssGetClean(rawQuery);log.info("修改后参数为:{}", rawQuery);try {//重新构造get requestURI newUri = UriComponentsBuilder.fromUri(uri).replaceQuery(rawQuery).build(true).toUri();ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();return chain.filter(exchange.mutate().request(request).build());} catch (Exception e) {log.error("get请求清理xss攻击异常", e);throw new IllegalStateException("Invalid URI query: \"" + rawQuery + "\"");}}//post请求时,如果是文件上传之类的请求,不修改请求消息体else if (postFlag) {return DataBufferUtils.join(serverHttpRequest.getBody()).flatMap(d -> Mono.just(Optional.of(d))).defaultIfEmpty(Optional.empty()).flatMap(optional -> {// 取出body中的参数String bodyString = "";if (optional.isPresent()) {byte[] oldBytes = new byte[optional.get().readableByteCount()];optional.get().read(oldBytes);bodyString = new String(oldBytes, StandardCharsets.UTF_8);}HttpHeaders httpHeaders = serverHttpRequest.getHeaders();// 执行XSS清理log.info("{} - [URL:{}] XSS处理前参数:{}", method, uri.getPath(), bodyString);bodyString = XssCleanRuleUtils.xssPostClean(bodyString);log.info("{} - [URL:{}] XSS处理后参数:{}", method, uri.getPath(), bodyString);ServerHttpRequest newRequest = serverHttpRequest.mutate().uri(uri).build();// 重新构造bodybyte[] newBytes = bodyString.getBytes(StandardCharsets.UTF_8);DataBuffer bodyDataBuffer = toDataBuffer(newBytes);Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);// 重新构造headerHttpHeaders headers = new HttpHeaders();headers.putAll(httpHeaders);// 由于修改了传递参数,需要重新设置CONTENT_LENGTH,长度是字节长度,不是字符串长度int length = newBytes.length;headers.remove(HttpHeaders.CONTENT_LENGTH);headers.setContentLength(length);headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf8");// 重写ServerHttpRequestDecorator,修改了body和header,重写getBody和getHeaders方法newRequest = new ServerHttpRequestDecorator(newRequest) {@Overridepublic Flux<DataBuffer> getBody() {return bodyFlux;}@Overridepublic HttpHeaders getHeaders() {return headers;}};return chain.filter(exchange.mutate().request(newRequest).build());});} else {return chain.filter(exchange);}}/*** 自定义过滤器执行的顺序,数值越大越靠后执行,越小就越先执行*/@Overridepublic int getOrder() {return Ordered.HIGHEST_PRECEDENCE;}/*** 字节数组转DataBuffer** @param bytes 字节数组* @return DataBuffer*/private DataBuffer toDataBuffer(byte[] bytes) {NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);buffer.write(bytes);return buffer;}}
参考资料
https://tech.meituan.com/2018/09/27/fe-security.html