安全测试的老哥说我的系统不抗揍

发表于:2022-4-01 09:28

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:柏炎    来源:稀土掘金

  Hello,这里是爱 Coding,爱 Hiphop,爱喝点小酒的 AKA 柏炎。
  我们部门每次大版本发布,都需要走一道公司的安全测试
  这不最近公司的安全测试标准提高了,我所负责的用户服务被一口气提了10个安全问题。

  不过用户中心是核心的底层业务服务,它的数据安全性与系统稳定性都是极其重要的,发现了Bug,我们只能逐个去修复了。
  本文将针对其中比较典型三个问题做分析与解决方案阐述。
  一、IP伪造
  日常业务开发的过程中,我们可能会需要获取请求接口的用户IP信息。
  为了防止黑客通过爆破的方式登陆系统,我将记录每一次用户登陆的IP,在一定时间范围内连续输入错误的用户名或者密码,将锁定IP。此IP在锁定时间内无法再请求登陆接口。
  修复前获取IP逻辑
    static String getIpAddr(HttpServletRequest request) {
           if (request == null) {
               return "unknown";
           }
           String ip = request.getHeader("x-forwarded-for");
           if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
               ip = request.getHeader("Proxy-Client-IP");
           }
           if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
               ip = request.getHeader("X-Forwarded-For");
           }
           if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
               ip = request.getHeader("WL-Proxy-Client-IP");
           }
           if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
               ip = request.getHeader("X-Real-IP");
           }
    
           return "0:0:0:0:0:0:0:1".equals(ip)   LOCAL_IP : ip;
   }
  
  从业务功能使用的角度上来看,这段代码没有任何问题,我们能够从HttpServletRequest中获取到报文中的IP数据。
  但是发现没有,我们获取的IP数据都是从请求头中获取的,而请求头的所有报文信息都是可以通过报文进行伪造的。只要攻击的黑客弄一个IP池,不断的变化,我们的防爆破机制就失效了。
  解决思路
  说实话,我当时为了完成这个IP获取需求,上面的代码也是直接百度了一份,发现能用也就用了。
  我并不知道Header中获取到的IP值的意思是什么(文中不阐述比如:Proxy-lient-IP这些请求头的含义)。
  不过好在安全测试给出了修复建议:
  IP数据获取需要从remoteAddr中获取。
  remote_addr 是服务端根据请求TCP包的ip指定的。假设从client到server中间没有任何代理,那么web服务器(Nginx,Apache等)就会把client的IP设为remote_addr;如果存在代理转发HTTP请求,web服务器会把最后一次代理服务器的IP设置为remote_addr。
  因为我们的服务都是统一走的nginx代理,所以可以在nginx中取到remote_addr,然后设置一个独立的业务请求头传递给用户中心。
  1.增加nginx配置
  2.编码实现
   /**
    * 获取真实ip,防止ip伪造
    *
    * @param request
    * @return
    */
   private static String getIpAddrFromRemoteAddr(HttpServletRequest request){
       String ip = request.getHeader("X-Real-IP");
       if (StringUtil.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
           return ip;
       }
       ip = request.getHeader("X-Forwarded-For");
       if (StringUtil.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
       // 多次反向代理后会有多个IP值,第一个为真实IP。
           int index = ip.indexOf(',');
           if (index != -1) {
               return ip.substring(0, index);
           } else {
               return ip;
           }
       } else {
           return request.getRemoteAddr();
       }
   }
 
  二、登陆未使用验证码
  基本上所有的登陆都会通过使用验证码的方式去防刷登陆接口。
  我们产品最开始不想要验证码逻辑,为了防止暴力破解密码。我们使用了同一IP不能连续失败的逻辑防止盗刷,但是新规范下,安全测试还是不认。
  没办法,他们掌握着我们的产品上架的生杀大权,我只能去加上验证码的功能。
  验证码的方案无非两种:前端生成验证码还是后端生成验证码。
  由于我们的前端大佬比较懒,只能我们后端生成验证码了。
  验证码的生成工具我选择了Hutool,开箱即用。
  先来看一下Hutool生成验证码的使用方式
   //定义图形验证码的长、宽、验证码字符数、干扰元素个数
   CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20);
    
   //获取验证码的base64
   String captchaImage = circleCaptcha.getImageBase64Data();
    
   //获取验证码
   String code = circleCaptcha.getCode();
  
  生成的验证码例如:
  简易版验证码前后端的校验逻辑:
  1.获取验证码接口
  前端请求后端生成验证码接口,后端生成验证码,将base64做为key,验证码code作为value保存至redis,然后返回base64给前端。
  2.登陆
  前端将用户输入的code与base64传到后端,校验base64在redis的值。
  三、DDos攻击
  验证码逻辑做完之后发现还是存在了一个攻击点。
  后端在生成验证码的时候是需要把base64作为redis的key存储到redis中的。
  高频请求验证码接口的情况下,大量的base64的key导致redis的响应变慢,甚至撑爆redis。
  这就是DDos攻击:
  一般来说是指攻击者利用“肉鸡”对目标网站在较短的时间内发起大量请求,大规模消耗目标网站的主机资源,让它无法正常服务。在线游戏、互联网金融等领域是 DDoS 攻击的高发行业。
  我们公司是安全公司,有专门的安全产品可以处理这种场景。
  那如果不购买对应的安全产品,我们如何在应用层面防止DDos攻击呢?
  DDos攻击就是高频的恶意请求,也就是高并发,高并发防刷你能想到什么?
  可不就是限流吗?
  3.1.网关限流
  如果你使用的是gateway网关作为业务请求的入口,你可以直接设置一个单位时间内同一ip请求同一个url的限流器。
  1.限流器
   @Configuration
   public class LimitConfig {
    
       @Bean
       @Primary
       KeyResolver hostResolver() {
           return exchange ->{
               ServerHttpRequest serverHttpRequest = Objects.requireNonNull(exchange.getRequest());
               return Mono.just(serverHttpRequest.getLocalAddress().getAddress().getHostAddress()+":"+serverHttpRequest.getURI().getPath());
           };
       }
    
   }
 
  2.增加限流过滤工厂类
   @Component
   @ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
   public class BaiyanRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory<BaiyanRateLimiterGatewayFilterFactory.Config> {
    
       private final RateLimiter defaultRateLimiter;
    
       private final KeyResolver defaultKeyResolver;
    
       public BaiyanRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter,
                                                     KeyResolver defaultKeyResolver) {
           super(Config.class);
           this.defaultRateLimiter = defaultRateLimiter;
           this.defaultKeyResolver = defaultKeyResolver;
       }
    
       public KeyResolver getDefaultKeyResolver() {
           return defaultKeyResolver;
       }
    
       public RateLimiter getDefaultRateLimiter() {
           return defaultRateLimiter;
       }
    
       @SuppressWarnings("unchecked")
       @Override
       public GatewayFilter apply(BaiyanRateLimiterGatewayFilterFactory.Config config) {
           return new InnerFilter(config,this);
       }
    
       /**
        * 内部配置加载类
        */
       public static class Config {
    
           private KeyResolver keyResolver;
    
           private RateLimiter rateLimiter;
    
           private HttpStatus statusCode = HttpStatus.TOO_MANY_REQUESTS;
    
           public KeyResolver getKeyResolver() {
               return keyResolver;
           }
    
           public BaiyanRateLimiterGatewayFilterFactory.Config setKeyResolver(KeyResolver keyResolver) {
               this.keyResolver = keyResolver;
               return this;
           }
    
           public RateLimiter getRateLimiter() {
               return rateLimiter;
           }
    
           public BaiyanRateLimiterGatewayFilterFactory.Config setRateLimiter(RateLimiter rateLimiter) {
               this.rateLimiter = rateLimiter;
               return this;
           }
    
           public HttpStatus getStatusCode() {
               return statusCode;
           }
    
           public BaiyanRateLimiterGatewayFilterFactory.Config setStatusCode(HttpStatus statusCode) {
               this.statusCode = statusCode;
               return this;
           }
    
       }
    
       /**
        * 内部类,用于指定限流过滤器的级别
        */
       private class InnerFilter implements GatewayFilter, Ordered {
    
           private Config config;
    
           private BaiyanRateLimiterGatewayFilterFactory factory;
    
           InnerFilter(BaiyanRateLimiterGatewayFilterFactory.Config config,BaiyanRateLimiterGatewayFilterFactory factory) {
               this.config = config;
               this.factory = factory;
           }
    
           @Override
           @SuppressWarnings("unchecked")
           public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
               KeyResolver resolver = (config.keyResolver == null)   defaultKeyResolver : config.keyResolver;
               RateLimiter<Object> limiter = (config.rateLimiter == null)   defaultRateLimiter : config.rateLimiter;
    
               Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
    
               return resolver.resolve(exchange).flatMap(key ->
                       limiter.isAllowed(route.getId(), key).flatMap(response -> {
    
                           for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
                               exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
                           }
    
                           if (response.isAllowed()) {
                               return chain.filter(exchange);
                           }
                           ServerHttpResponse rs = exchange.getResponse();
                           byte[] datas = GsonUtil.gsonToString(Result.error(429,"too many request","访问过快",null))
                                   .getBytes(StandardCharsets.UTF_8);
                           DataBuffer buffer = rs.bufferFactory().wrap(datas);
                           rs.setStatusCode(HttpStatus.UNAUTHORIZED);
                           rs.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
                           return rs.writeWith(Mono.just(buffer));
                       }));
           }
    
           @Override
           public int getOrder() {
               return GatewayFilterOrderConstant.RATE_LIMITER_FILTER;
           }
       }
    
   }
  
  3.增加配置
   spring:
     cloud:
       gateway:
         # 网关路由策略
         routes:
           - id: auth
             uri: lb://auth
             predicates:
               - Path=/api/**
             filters:
               #限流配置
               - name: BaiyanRateLimiter
                 args:
                   # 每秒补充10个
                   redis-rate-limiter.replenishRate: 10 
                   # 突发20个
                   redis-rate-limiter.burstCapacity: 20
                   # 每次请求消耗1个
                   redis-rate-limiter.requestedTokens: 1 
                   key-resolver: "#{@hostResolver}"
 
  3.2.应用限流
  没有使用网关的系统,我们可以单独使用AOP,过滤器,或者拦截器的方式进行的单应用服务限流。
  思路其实与网关限流很类似。
  成熟的限流方案有滑动窗口、令牌桶或者漏桶,不做展开讲解。
  四、总结
  本文针对我在工作中碰到的三个安全测试问题做了详细的问题描述,并针对问题进行分析逐步得到解决方案。
  现将问题与解决方案总结如下:

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号