背景
交付场景中安全问题非常重要。基础技术组现在基于spring boot starter,开发了安全二方库,用于解决在生产环境中常见的安全问题。
主要功能
● CSRF防范
● XSS防范
● SQL注入防范
● 路径遍历防范
● 系统命令及参数过滤
● 防止重复提交

代码仓库地址
https://code.dayu.work/dayu-security/dayu-security-all.git

maven依赖

 <dependency>
   <groupId>com.aliyun.gts.bpaas</groupId>
   <artifactId>aliyun-gts-security-spring-boot-starter</artifactId>
   <version>2.1.13.3-SNAPSHOT</version>
</dependency>

如何使用
引入如上依赖后,即可在项目中使用。现在分别介绍如何使用该二方库,针对各个安全漏洞进行代码层面上的编码。

CSRF
CSRF是什么
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
CSRF可以做什么
你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账…造成的问题包括:个人隐私泄露以及财产安全。

CSRF原理
参考文档:https://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html

防范
CSRF主要防范方式是在前端http请求中配置一个能够被服务器验证的token。

配置
在这里插入图片描述

引入二方库以后,返回的cookie中包含名为XSRF-TOKEN的token,请前端从cookie中获取该token,并在请求的header中,带入该token,参数名为X-XSRF-TOKEN否则方法会抛出异常。

XSS
XSS是什么?
XSS漏洞是Web应用程序中最常见的漏洞之一。
XSS是指恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。
参考文档:https://www.jianshu.com/p/4fcb4b411a66

二方库使用
在方法或者Controller类上加入注解XssResponseFilter,返回的对象中String类型的字段会被执行html过滤。
例如如下代码:

@PostMapping("/testFilter")
@XssResponseFilter
public ResultResponse<String> testFilter(@RequestBody RequestDto requestDto) {
   String requestStr = JSON.toJSONString(requestDto);
   ResultResponse<String> r = new ResultResponse<>();
   r.setData(requestStr);
   return r;
}

当前端发送请求内容如下:

{
  "json": "ha"
}

会返回:

{
  "message":"success",
  "data":"{&quot;json&quot;:&quot;ha&quot;}",
}

json字符串被进行了转义。

配置
在这里插入图片描述
防止重复提交(单机版)
使用
我们常常需要防止重复提交,例如多次点击造成订单重复提交,或者造成重复汇款等操作。本组件通过注解配置的方式来实现方法的幂等效果。

例子:

@NoDuplicateSubmission(lockKey = "#id")
public void testMethod(long id) {
}

默认情况下,会自动根据类名、方法名、参数hash值来作为重复提交判断的key。 如果需要自定义缓存key,可通过注解中的参数lockKey来指定(支持spel表达式)。

关于在分布式环境下防止重复提交,请参考缓存中间件(aliyun-gts-middleware-cache-starter)中的分布式锁注解"@GTSDistributedLock"。
配置
在这里插入图片描述
防路径遍历
路径遍历的主要问题是用户通过构造非法的路径请求(例如图片的路径),访问未授权的服务器本地文件地址。这种情况主要是由于没有对用户输入的参数的合法性进行校验。

使用
在Controller方法的参数上加入注解@PathFilter,会对参数执行路径遍历过滤。 需注意加了@PathFilter注解就不需要加@RequestParam注解了,不然会导致自定义注解失效。

例子:

    @GetMapping("/path-traversal/secure/path-filter")
    public ResultResponse<String> getImageSecByPathFilter(@PathFilter String filePath) throws IOException {
        ResultResponse<String> r = new ResultResponse<>();
        r.setData(getImgBase64(filePath));
        return r;
    }

发送请求http://localhost:8080/path-traversal/secure/path-filter?filePath=…/…/…/…/…/…/etc/passwd,会返回报错。

{
    "timestamp": "2021-02-19T05:55:36.736+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "The path argument cannot contain relative or absolute path",
    "path": "/path-traversal/secure/path-filter"
}

SecurityUtils
包含一系列常用工具方法用于过滤可能包含非法字符的输入。

转义命令行参数
如果你的代码中会运行一个命令行,同时命令行的参数来自用户输入,请使用如下方法来过滤参数本身。

// 输出 'cat /password',带引号
SecurityUtils.escapeShellArg("cat /password")

HTML安全转义函数
对HTML字符串进行转义。

String escaped = SecurityUtils.escapeHtml("<html></html>");
assertEquals("&lt;html&gt;&lt;/html&gt;", escaped);

XML安全转义函数
执行xml 1.1安全转义。

String escaped = SecurityUtils.escapeXml("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
assertEquals("&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;", escaped);

JSON安全转义函数

String escaped = SecurityUtils.escapeJson("{\"name\": \"li\", \"first name\": \"chen\"}");
assertEquals("\\u007B\\\"name\\\"\\u003A\\u0020\\\"li\\\"\\u002C\\u0020\\\"first\\u0020name\\\"\\u003A\\u0020"
            + "\\\"chen\\\"\\u007D", escaped);

下面是SQL拼接时候推荐使用的方法。如果使用mybatis,使用 # 替代 $

List<User> users = userMapper.findByUserNameSecure2(SecurityUtils.escapeSql(keyword));

SQL order by参数过滤函数

List<User> users = userMapper.findOrderByVulnerable3(SecurityUtils.escapeSql(orderBy));

防篡改和重放
前端组件使用方法
具体请查看:https://dayu.work/devops/materiel/fe/componentDetail/3257
配置项
在这里插入图片描述
使用
1.需要将gts.security.sign-check.enable的值设置为true
2.需要实现缓存接口cacheInterface

public interface CacheInterface {

    /**
     * 判断缓存中是否拥有此值
     * @param key 键
     * @return 是否拥有
     */
    boolean hasKey(String key);

    /**
     * 写入缓存
     * @param key  键
     * @param value 值
     * @param timeToLive 存在时常
     * @param timeUnit 时间单位
     */
    void setKey(String key, String value, long timeToLive, TimeUnit timeUnit);
}

推荐使用中间件中的缓存组件实现,具体内容参考
https://iwhale-citybrain.yuque.com/altet6/dl2a5i/vfqn74

实现example

@Service
public class CacheService implements CacheInterface {

    @Autowired
    CacheManager cacheManager;

    @Override
    public boolean hasKey(String key) {
        return cacheManager.hasKey(key);
    }

    @Override
    public void setKey(String key, String value, long timeToLive, TimeUnit timeUnit) {
        cacheManager.set(key,value,timeToLive,timeUnit);
    }
}

此实现类需要使用@Service或者@Component来注入
在开启防篡改和重放后,如果未实现cacheInterface或者该实现类未注入将启动报错
在这里插入图片描述
3.签名加解密方式
gts.security.sign-check.signMethod的值需要与前端约定保持一致,否则将数字签名失败

校验步骤
a. 前端发送两个header,一个是gts-ca-timestamp,获取当前发送时间;一个是gts-ca-nonce,一般是md5(timestamp + random()或者一个uuid)。

b. 后端收到timestamp以后,判断和当前系统时间的差距,如果过大,认为超时。

c. 检查nouce,如果在分布式cache中不存在,放在分布式cache中,expire时间为timestamp。如果存在,认为是重复提交。

错误说明
a.参数校验
header中未包含签名gts-ca-signature
返回

{
    "message": "签名不允许为空",
    "success": false
}

header中未包含签名gts-ca-timestamp
返回

{
    "message": "时间戳不允许为空",
    "success": false
}

header中未包含签名gts-ca-nonce
返回

{
    "message": "客户端随机值不可为空",
    "success": false
}

b.过期校验
返回

{
    "message": "已过期的签名",
    "success": false
}

c.重放校验
返回

{
    "message": "重复请求",
    "success": false
}

d.安全签名校验
返回

{
    "message": "数字签名失败",
    "success": false
}

试用地址
http://dayu-sign-check-demo-daily.ingress.dayu.work/home