Брат тестировщика безопасности сказал, что моя система не устойчива к взлому

задняя часть Безопасность

Здравствуйте, это Бо Ян, он же Бо Ян, который любит программирование, хип-хоп и напитки.

Каждый раз, когда наш отдел выпускает основную версию, нам необходимо пройти проверку безопасности компании.

Это не потому, что стандарты тестирования безопасности компании в последнее время повысились, а пользовательскому сервису, за который я отвечаю, было задано 10 проблем с безопасностью за один раз.

image-20220109142604456

Молодец, 3.25 не поехал.

img

Однако пользовательский центр является основным базовым бизнес-сервисом, и его безопасность данных и стабильность системы чрезвычайно важны.Ошибки обнаруживаются, и мы можем исправлять их только одну за другой.

В этой статье будут проанализированы и объяснены решения трех типичных проблем.

image-20220109102353145

1. Подделка ИС

В процессе ежедневного развития бизнеса нам может понадобиться получить информацию об 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 в то время, я также напрямую скопировал приведенный выше код из Baidu и использовал его, когда обнаружил, что его можно использовать.

Я не знаю, что означает значение IP, полученное в заголовке(文中不阐述比如:Proxy-lient-IP这些请求头的含义).

К счастью, тест безопасности дал рекомендации по ремонту:

Получение данных IP должно быть получено от remoteAddr.

remote_addr указывается сервером в соответствии с IP-адресом запрашивающего TCP-пакета. Предполагая, что нет прокси от клиента к серверу, веб-сервер (Nginx, Apache и т. д.) установит IP-адрес клиента в remote_addr; если есть прокси, пересылающий HTTP-запросы, веб-сервер установит последний прокси-сервер. IP для удаленного_адреса.

Поскольку все наши сервисы являются унифицированными прокси-серверами nginx, мы можем получить remote_addr в nginx, а затем установить независимый заголовок бизнес-запроса для передачи в пользовательский центр.

1. Увеличьте конфигурацию nginx

image-20220109114230543

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();
     }
 }

2. Войти без кода подтверждения

По сути, все входы в систему будут использовать код подтверждения, чтобы предотвратить очистку интерфейса входа в систему.

Наш продукт не хотел использовать логику проверочного кода в начале, чтобы предотвратить взлом пароля методом перебора. Мы используем логику, согласно которой один и тот же IP не может постоянно выходить из строя, чтобы предотвратить мошенничество, но в соответствии с новой спецификацией тест безопасности все равно не распознает его.

image-20220109115050262

Ни в коем случае, они держат в руках жизнь и смерть наших товаров на полках, могу только добавить функцию проверочного кода.

img

Существует всего две схемы проверочных кодов: фронтенд генерирует проверочные коды или бэкенд генерирует проверочные коды.

Поскольку наши фронтенд-боссы ленивы, мы можем генерировать проверочные коды только на бэкенде.

Инструмент генерации кода подтверждения, который я выбралHutool, из коробки.

Давайте посмотрим, как Hutool генерирует коды подтверждения.

 //定义图形验证码的长、宽、验证码字符数、干扰元素个数
 CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20);
 ​
 //获取验证码的base64
 String captchaImage = circleCaptcha.getImageBase64Data();
 ​
 //获取验证码
 String code = circleCaptcha.getCode();

Сгенерированный проверочный код, такой как

16113807_sICp

Логика проверки передней и задней части упрощенной версии проверочного кода:

1. Интерфейс получения кода подтверждения

Внешняя часть запрашивает внутреннюю часть для создания интерфейса кода проверки, внутренняя часть генерирует код проверки, использует base64 в качестве ключа и сохраняет код кода проверки в качестве значения для повторного использования, а затем возвращает base64 на передний план. -конец

2. Войти

Внешний интерфейс передает код и base64, введенные пользователем, на задний конец и проверяет значение base64 в redis.

3. DDoS-атака

После завершения логики проверочного кода обнаруживается, что точка атаки все еще существует.

Когда серверная часть генерирует код подтверждения, ему необходимо сохранить base64 в качестве ключа Redis в Redis.

В случае частых запросов интерфейса кода проверки большое количество ключей base64 приводит к замедлению отклика redis, а то и к взрыву redis.

это DDos атака

Вообще говоря, это означает, что злоумышленник использует «бройлер» для инициирования большого количества запросов к целевому веб-сайту за короткий период времени, потребляя ресурсы хоста целевого веб-сайта в больших масштабах, что делает его неспособным нормально обслуживать. . Онлайн-игры, интернет-финансы и другие области являются сферами, где часто происходят DDoS-атаки.

Наша компания является охранной компанией и имеет специализированные продукты для обеспечения безопасности, чтобы справиться с этим сценарием.

Если мы не купим соответствующие продукты безопасности, как мы можемУровень приложенияКак насчет предотвращения DDoS-атак?

DDos-атаки — это высокочастотные вредоносные запросы, то есть high concurrency, high concurrency anti-brushing, что вы можете придумать?

Разве это не просто текущий предел?

img

3.1 Ограничение тока шлюза

Если вы используете шлюз шлюза в качестве записи для бизнес-запросов, вы можете напрямую установить текущий ограничитель, который запрашивает один и тот же URL-адрес с одного и того же IP-адреса в единицу времени.

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 Ограничение тока приложения

Без системы, использующей шлюз, мы можем использовать только АОП, фильтры или перехватчики, чтобы ограничить поток служб с одним приложением.

Идея на самом деле очень похожа на ограничение тока шлюза.

Зрелые схемы ограничения тока включают скользящие окна, ведра с маркерами или дырявые ведра, которые не будут объяснены.

4. Резюме

В этой статье дается подробное описание трех проблем тестирования безопасности, с которыми я столкнулся в своей работе, и анализируются проблемы для пошагового решения.

Проблемы и решения резюмируются следующим образом.

image-20220109131752165

5. Свяжитесь со мной

Если есть неточности в тексте, поправьте меня, текст писать непросто, ставьте лайк, ладно~

WeChat: baiyan_lou

Общественный номер: дядя Бай Ян