背景
交付场景中安全问题非常重要。基础技术组现在基于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":"{"json":"ha"}",
}
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("<html></html>", escaped);
XML安全转义函数
执行xml 1.1安全转义。
String escaped = SecurityUtils.escapeXml("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
assertEquals("<?xml version="1.0" encoding="UTF-8"?>", 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