API接口防止参数篡改和重放攻击

10,904 阅读7分钟

 API重放攻击(Replay Attacks)又称为重播攻击、回放攻击。它的原理就是把之前窃听到的数据原封不动的重新发送给接收方。HTTPS并不能防止这种攻击,虽然传输的数据都是经过加密的,窃听者无法得到数据的准确定义,但是可以从请求的接收方地址分析这些数据的作用。比如用户登录请求时攻击者虽然无法窃听密码,但是却可以截获加密后的口令然后将其重放,从而利用这种方式进行有效的攻击。

所谓重放攻击就是攻击者发送一个目的主机已经接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,重放攻击是计算机世界黑客常用的攻击方式之一。

一次HTTP请求,从请求方到接收方中间要经过很多路由器和交换机,攻击者可以在中途截获请求的数据。假设在一个网上存款系统中,一条消息表示用户支取了一笔存款,攻击者完全可以多次发送这条消息而偷窃存款。

重放是二次请求,如果API接口没有做到对应的安全防护,将可能造成很严重的后果。

API接口常见的安全防护要做到主要有以下几点:

  • 防止sql注入
  • 防止xss攻击
  • 防止请求参数被篡改
  • 防止重放攻击

主要防御措施可以归纳为两点:

  • 对请求的合法性进行校验
  • 对请求的数据进行校验

防止重放攻击必须要保证请求仅一次有效。需要通过在请求体重携带当前请求的唯一标识,并且进行签名防止被篡改。所以防止重放攻击需要建立在防止签名被篡改的基础之上。

请求参数防篡改

采用https协议可以将传输的明文进行加密,但是黑客仍然可以截获传输的数据包,进一步伪造请求进行重放攻击。如果黑客使用特殊手段让请求方设备使用了伪造的证书进行通信,那么https加密的内容也会被解密。

在API接口中我们除了使用https协议进行通信外,还需要有自己的一套加解密机制,对请求的参数进行保护,防止被篡改。

过程如下:

  1. 客户端使用约定好的秘钥对传输的参数进行加密,得到签名值signature,并且将签名值也放入请求的参数中,发送请求给服务端
  2. 服务端接收到客户端的请求,然后使用约定好的秘钥对请求的参数(除了signature以外)再次进行签名,得到签名值autograph。
  3. 服务端比对signature和autograph的值,如果对比一致,认定为合法请求。如果对比不一致,说明参数被篡改,认定是合法请求。

因为黑客不知道签名的秘钥,所以即使截获到请求数据,对请求参数进行篡改,但是却无法对参数进行前面,无法得到修改后参数的签名值signature。 签名的秘钥我们可以使用很多方案,可以采用对称加密或者非对称加密

防止重放攻击

基于timestamp的方案

每次HTTP请求,都需要加上timestamp参数,然后把timestamp和其他参数一起进行数字签名。因为一次正常的HTTP氢气u,从发出到达服务器一般都不会超过60s,所以服务器收到HTTP请求之后,首先判断时间戳参数与当前时间比较,是否超过了60s,如果超过了则认为是非法请求。

一般情况下,黑客从抓包重放请求耗时远远超过了60s,所以此时请求中的timestamp参数已经失效了。 如果黑客修改timestamp参数为当前的时间戳,则signature参数对应的数字签名就会失效,因为黑客不知道签名秘钥,没有办法生成新的数字签名。

但是这种方式的漏洞也是显而易见,如果在60s之内进行重放攻击,那就没办法了,所以这种方式不能保证请求仅一次有效。

基于nonce的方案

nonce的意思是仅一次有效的随机字符串,要求每次请求时,该参数要保证不同,所以该参数一般与时间戳有关,我们这里为了方便起见,直接使用时间戳的16进制,实际使用客户加上客户端的ip地址,mac地址等信息做个哈希之后,作为nonce参数。

我们每次将请求的nonce参数存储到一个”集合“中,可以json格式存储到数据库或缓存。

每次处理HTTP请求时,首先判断该请求的nonce是否在该集合中,如果存在则认为是非法请求。

nonce参数在首次请求时,已经被存储到了服务器上的集合中,再次请求会被识别并拒绝。

nonce参数作为数组签名的一部分,是无法篡改的,因为黑客不清楚token,所以不能生成新的sign。

这种方式也有很大的问题,那就是存储nonce参数的集合会越来越大,验证nonce是否在集合中的耗时会越来越长。我们不能让nonce集合无限大,所以需要定期清理该集合,但是一旦该集合被清理,我们就无法验证被清理的nonce参数了。也就是说,假设该集合平均1天清理一次的话,我们抓取到的该url虽然当时无法进行重放攻击,但是我们还是可以每隔一天进行一次重复刚攻击的。而且存储24小时内,所有请求的nonce参数,也是一笔不小的开销。

基于timestamp和nonce的方案

nonce的一次性可以解决timestamp参数60s的问题,timestamp可以解决nonce参数集合越来越大的问题。防止重放攻击一般和防止请求参数被篡改一起做。请求的Headers数据如下图所示。 我们在timestamp方案的基础上,加上nonce参数,因为timestamp参数对于超过60s的请求,都认为是非法请求,所以我们只需要存储60s的nonce参数集合即可。

API接口验证流程:

String token = request.getHeader("token");
String timestamp = request.getHeader("timestamp");
String nonceStr = request.getHeader("nonceStr");

String url = request.getHeader("url");

String signature = request.getHeader("signature");


if(StringUtil.isBlank(token) || StringUtil.isBlank(timestamp) || StringUtil.isBlank(nonceStr) || StringUtil.isBlank(url)
|| StringUtil.isBlank(signature))
{
    return;
}

UserTokenInfo userTokenInfo = TokenUtil.getUserTokenInfo(token);

if(userTokenInfo == null){
    return;
}

if(!request.getRequestURI().equal(url)){
return;
}

if(DateUtil.getSecond()-DateUtil.toSecond(timestamp) > 60){
    return;
}

if(RedisUtils.haveNonceStr(userTokenInfo,nonceStr)){
    return;
}

String stringB = SignUtil.signature(token, timestamp, nonceStr, url, request);
if(!signature.equals(stringB)){
    return;
}
RedisUtils.saveNonceStr(userTokenInfo,nonceStr,60);

微信公众号如何保证消息不会被重放攻击

使用微信公众平台的接口需要在微信公众平台设置token。这里假设token是不会被攻击者知道的,相当于一个PSK(Pre Shared Key) 微信发消息会有三个参数:signature、timestamp和nonce。

验证是否为微信发送的消息流程如下:

$signature = $_GET["signature"];
$timestamp = $_GET["timestamp"];
$nonce = $_GET["nonce"];
$token = TOKEN;
// 按照$token,$timestamp,$nonce的顺序组成数组
$tmpArr = array($token, $timestamp, $nonce);
// 按照字典序排序
sort($tmpArr, SORT_STRING);
// 将排序后的数组串成字符串
$tmpStr = implode( $tmpArr );
// 用sha1计算签名
$tmpStr = sha1( $tmpStr );
// 校验签名
if( $tmpStr == $signature ){
    return true;
}else{
    return false;
}

这里在使用的时候,这里针对消息的重放攻击的防护应该检查nonce值是否已经存在,如果已经存在可能为非法通知,按道理来讲也应该检查这里timestamp和当前时间比较是否已经过了一定的时间,但是这里在微信公众号开发文档中并没有说明,这里其实应该是需要验证消息是否是否在同一个时间戳内的,如果接收到的消息距离发送的时间已经超过1s就可以直接丢弃了,当然在网络不好的情况下会造成丢消息。

另外一个这里针对重试或者幂等性的要求上要做到解密后消息中去,比如发起重试使用同样的消息id即可。虽然每次的nonce值是不一样的,但是消息的id是一样的,就可以区分出来是否为同一个消息的重试发送。

这里一般的做法是抽出一个前置网关,做消息的解密和加密,后端服务进行消息的处理,消息的处理上对重试带来的幂等性问题做处理就可以了。