Hello, this is AKA Baiyan who loves Coding, Hiphop and drinking a little wine.

Every time our department releases a big version, we need to go through a company’s security test.

Recently, the company’s security testing standards have been raised, and the user service I am responsible for has received 10 security questions in one sitting.

Boy, 3.25 is dead.

However, the user center is the core underlying business service, and its data security and system stability are extremely important. If bugs are found, we can only fix them one by one.

In this paper, three typical problems are analyzed and their solutions are described.

I. IP forgery

During daily business development, we may need to obtain the IP information of the user requesting the interface.

In order to prevent hackers from logging in the system by blasting, I will record the IP of each user logging in, and the IP will be locked if the user name or password is entered incorrectly in a certain period of time. This IP address cannot request login to the interface during the lock period.

Obtain IP logic before repair

  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;
 }
Copy the code

From a business function usage point of view, there is nothing wrong with this code, we can get the IP data in the message from HttpServletRequest.

However, it was not found that the IP data we obtained were all obtained from the request header, and all the packet information in the request header could be forged by the packet. As soon as the hacker gets a pool of IP addresses and keeps changing, our anti-explosion mechanism fails.

solution

To tell the truth, I was in order to complete the IP acquisition requirements, the above code is also directly baidu a copy, found that can also use.

I don’t know what the IP value retrieved from the Header means (I don’t explain the meaning of proxy-lient-ip headers for example).

Fortunately, security tests suggest fixes:

The IP data needs to be retrieved from remoteAddr.

Remote_addr is specified by the server based on the IP address of the REQUESTED TCP packet. Assuming there is no proxy between client and server, the Web server (Nginx, Apache, etc.) will set the client’s IP to remote_ADDR; If there is a proxy forwarding HTTP request, the Web server sets the last proxy server IP to remote_ADDR.

Because our services are all nginx proxies, we can fetch remote_ADDR in nginx and set a separate business request header to pass to the user center.

1. Add the nginx configuration

2. Coding implementation

/** * Get the real IP address, * * @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)) {// After multiple reverse proxies, there are multiple IP addresses. The first one is the real IP address. int index = ip.indexOf(','); if (index ! = -1) { return ip.substring(0, index); } else { return ip; } } else { return request.getRemoteAddr(); }}Copy the code

2. No verification code is used for login

Basically all logins will use a verification code to prevent the brush login interface.

We didn’t want captcha logic in the beginning, to prevent brute force cracking. We used the logic that the same IP cannot fail continuously to prevent stolen brush, but under the new specification, the security test is still not recognized.

I had to add captchas because they had the power of life and death over our product.

The verification code can be generated in either of the following ways: front-end or back-end.

Since our front-end boss is lazy, we have to generate captcha on the back end.

Verification code generation tool I chose Hutool, out of the box.

Let’s take a look at how Hutool generates captcha

/ / define the graphical verification code length, width and number CircleCaptcha captcha verification code number of characters, and interference elements. = CaptchaUtil createCircleCaptcha (200, 100, 4, 20). / / get the captcha base64 String captchaImage = circleCaptcha. GetImageBase64Data (); String code = circlecaptcha.getCode ();Copy the code

The generated verification code is an example

The verification logic of the front and back ends of the simple version verification code:

1. Obtain the verification code interface

The front end requests the back end to generate the verification code interface, the back end generates the verification code, saves base64 as the key, and the verification code as the value to Redis, and then returns Base64 to the front end

2. Log in

The front end transmits the code and base64 input by the user to the back end, and verifies the value of Base64 in Redis

3. DDos attacks

After the captcha logic is done, there is still a point of attack.

When generating the verification code, the backend needs to store Base64 as the key of Redis in Redis.

In the case of high-frequency request verification code interface, a large number of Base64 keys lead to slow response of REDis, or even burst Redis.

This is a DDos attack

Generally speaking, the attacker uses “chicken” to launch a large number of requests to the target website in a short period of time, consuming the host resources of the target website on a large scale and making it unable to provide normal services. Online games, Internet finance and other sectors are prone to DDoS attacks.

Our company is a security company, and we have special security products to deal with this kind of scenario.

So how can we prevent DDos attacks at the application level without purchasing security products?

DDos attacks are high frequency malicious requests, high concurrency, high concurrency brush-off what can you think of?

Isn’t it about limiting the flow?

3.1. Gateway traffic limiting

If you are using the Gateway gateway as an entry point for business requests, you can simply set up a traffic limiter that requests the same URL from the same IP address for a unit of time.

1. Current limiter

@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()); }; }}Copy the code

2. Add traffic limiting and filtering factories

@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 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 requests "," access too fast ",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; }}}Copy the code

3. Add configurations

Spring: Cloud: gateway: # Routes: -id: auth uri: lb://auth predicates: -path =/ API /** filters: # limiting filters - name: BaiyanRateLimiter ARgs: # replenishment of 10 Redis-rate-limiter. ReplenishRate: 10 # Burst of 20 Redis-rate-limiter. BurstCapacity: 20 # redis-rate-limiter. RequestedTokens: 1 key-resolver: #{@hostresolver}Copy the code

3.2. Apply current limiting

Without systems that use gateways, we can use AOP, filters, or interceptors alone for single-application service limiting.

The idea is actually very similar to gateway limiting.

Mature flow limiting schemes include sliding Windows, token buckets or leaky buckets, which will not be explained.

Four,

In this paper, I encountered three security testing problems in the work to do a detailed description of the problem, and for the problem analysis and gradually get solutions.

The problems and solutions are summarized as follows

Contact me

If there is an incorrect place in the article, welcome to correct, writing the article is not easy, point a thumbs-up, yao yao da ~

WeChat: baiyan_lou

Public account: Uncle Baiyan