The source of client data security

Posted by BlueNosE on Thu, 27 Jan 2022 23:51:05 +0100

Any data transmitted from the client cannot be trusted directly

1. The data transmitted from the client to the server is only information collection,
2. Data can only be used after validity verification, permission verification, etc,
3. Moreover, these data can only be considered as the intention of user operation, and can not directly represent the current state of the data.

The client's calculation is not trusted

@Data
public class Order {
    private long itemId; //commodity ID
    private BigDecimal itemPrice; //commodity price
    private int quantity; //Quantity of goods
    private BigDecimal itemTotalPrice; //Total price of goods
}

The first one: for the Order passed in from the front end, what is really directly used and reliable is the commodity ID and quantity passed in from the client, and the server will recalculate the final total price according to this information.

@PostMapping("/orderRight")
public void right(@RequestBody Order order) {
    //according to ID Re query goods
    Item item = Db.getItem(order.getItemId());
    //When the unit price of the goods passed in by the client does not match the unit price of the goods queried by the server, a friendly prompt will be given
    if (!order.getItemPrice().equals(item.getItemPrice())) {
        throw new RuntimeException("The price of the goods you purchased has changed, please place a new order");
    }
    //Reset commodity unit price
    order.setItemPrice(item.getItemPrice());
    //Recalculate the total price of goods
    BigDecimal totalPrice = item.getItemPrice().multiply(BigDecimal.valueOf(order.getQuantity()));
    //When the total price of goods passed in by the client does not match that queried by the server, a friendly prompt will be given
    if (order.getItemTotalPrice().compareTo(totalPrice)!=0) {
        throw new RuntimeException("The total price of the goods you purchased has changed. Please place a new order");
    }
    //Reset total price of goods
    order.setItemTotalPrice(totalPrice);
    createOrder(order);
}

The second is to redefine POJO CreateOrderRequest as the input parameter of the interface, which is more reasonable than using the domain model Order directly.

@Data
public class CreateOrderRequest {
    private long itemId; //commodity ID
    private int quantity;  //Quantity of goods
}

@PostMapping("orderRight2")
public Order right2(@RequestBody CreateOrderRequest createOrderRequest) {
    //commodity ID And the quantity of goods are reliable. No problem. Other data needs to be calculated by the server
    Item item = Db.getItem(createOrderRequest.getItemId());
    Order order = new Order();
    order.setItemPrice(item.getItemPrice());
   order.setItemTotalPrice(item.getItemPrice().multiply(BigDecimal.valueOf(order.getQuantity())));
    createOrder(order);
    return order;
}

The parameters submitted by the client need to be verified

@PostMapping("/right")
@ResponseBody
public String right(@RequestParam("countryId") int countryId) {
    if (countryId < 1 || countryId > 3)
        throw new RuntimeException("illegal parameter");
    return allCountries.get(countryId).getName();
}

Or use Spring Validation to verify parameters in the way of annotation, which is more elegant:

@Validated
public class TrustClientParameterController {
  @PostMapping("/better")
    @ResponseBody
    public String better(
            @RequestParam("countryId")
            @Min(value = 1, message = "illegal parameter")
            @Max(value = 3, message = "illegal parameter") int countryId) {
        return allCountries.get(countryId).getName();
    }
}

You cannot trust anything in the request header

@Slf4j
@RequestMapping("trustclientip")
@RestController
public class TrustClientIpController {

    HashSet<String> activityLimit = new HashSet<>();

    @GetMapping("test")
    public String test(HttpServletRequest request) {
        String ip = getClientIp(request);
        if (activityLimit.contains(ip)) {
            return "You have received the prize";
        } else {
            activityLimit.add(ip);
            return "Prize collection successful";
        }
    }

    private String getClientIp(HttpServletRequest request) {
        String xff = request.getHeader("X-Forwarded-For");
        if (xff == null) {
            return request.getRemoteAddr();
        } else {
            return xff.contains(",") ? xff.split(",")[0] : xff;
        }
    }
}

This implementation method that relies too much on the X-Forwarded-For request header to judge the uniqueness of users is problematic:

You can simulate the request through tools like cURL and tamper with the content of the header at will:

It can pass cURL Similar tools are used to simulate requests and tamper with the contents of headers at will:

Internet cafes, schools and other institutions often have the same export IP. In this scenario, only the user who first opens this page can receive the prize, while other users will be blocked.

Therefore, the IP address or any information in the request header, including the information in the Cookie and the Referer, can only be used as a reference and cannot be used as the basis for important logical judgment. For the uniqueness judgment needs of similar cases, the better way is to let the user log in or log in with three-party authorization (such as wechat) and get the user ID to judge the uniqueness.

The user ID cannot be obtained from the client

@GetMapping("wrong")
public String wrong(@RequestParam("userId") Long userId) {
    return "Current user Id: " + userId;
}

Case:

For a large project, the server directly uses the user ID transmitted from the client, which leads to security problems.

1. Do not correctly understand the users of the interface or service. If the interface faces the internal service, it is not unreasonable for the service caller to pass in the user ID, but such an interface cannot be directly opened to the client or H5.

2. In the testing phase, in order to facilitate testing and debugging, we usually implement some interfaces that can be used without login, directly use the user ID transmitted from the client, but forget to delete similar super interfaces before going online.

3. The front end of a large website may be composed of different modules, not necessarily a system, and the user login status may not be connected. Sometimes, we may directly transfer the user ID in the URL to get through the user login status through the front-end value transfer.

If the interface faces the user directly (such as calling the client or H5 page), the user must log in before using it.

After login, the user ID is saved in the server, and the interface needs to be obtained from the server (such as Session).

Here is a code that demonstrates the simplest login operation. After login, set the ID of the current user in the Session:

@GetMapping("login")
public long login(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession session) {
    if (username.equals("admin") && password.equals("admin")) {
        session.setAttribute("currentUser", 1L);
        return 1L;
    }
    return 0L;
}

Define a custom annotation @ LoginRequired to the userId parameter, and then automatically assemble the parameters through the HandlerMethodArgumentResolver:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Documented
public @interface LoginRequired {
    String sessionKey() default "currentUser";
}

After logging in, all methods that need to log in can get the user ID by adding @ LoginRequired annotation with one click, which is convenient and safe:

@Slf4j
public class LoginRequiredArgumentResolver implements HandlerMethodArgumentResolver {
    //Which parameters are resolved
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        //Matching parameter has@LoginRequired Annotation parameters
        return methodParameter.hasParameterAnnotation(LoginRequired.class);
    }


    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        //Get comments from parameters
        LoginRequired loginRequired = methodParameter.getParameterAnnotation(LoginRequired.class);
        //According to the Session Key,from Session Query user information in
        Object object = nativeWebRequest.getAttribute(loginRequired.sessionKey(), NativeWebRequest.SCOPE_SESSION);
        if (object == null) {
            log.error("Interface {} Illegal call!", methodParameter.getMethod().toString());
            throw new RuntimeException("Please login first!");
        }
        return object;
    }
}

Implement the addArgumentResolvers method of the WebMvcConfigurer interface to add this custom processor LoginRequiredArgumentResolver:

SpringBootApplication
public class CommonMistakesApplication implements WebMvcConfigurer {
...
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginRequiredArgumentResolver());
    }
}

Get through the user identity of different systems and even different websites

There are roughly three schemes to get through the login of users between different systems.

First, put the user identity on a unified server. Each system needs to go to this server to confirm the login status. After confirmation, save the session in the Cookie of its own website. This is the practice of single sign on. This scheme requires all associated systems to connect to a set of central authentication server (central save user session), and jump to the central authentication server for login or login status confirmation when they are not logged in. Therefore, this scheme is suitable for websites under different domain names within a company.

Second, the user's identity information is directly placed in the Token and transmitted arbitrarily at the client. The Token is verified by the server (if the key is shared, it does not even need to be verified by the same server). There is no need to use a central authentication server. It is relatively loosely coupled. The typical standard is JWT. This scheme is suitable for cross system user authentication of heterogeneous systems, and the user experience will be better than the single sign on scheme.

Third, if it is necessary to get through the user login status of different company systems, the authorization code mode in OAuth 2.0 standard will generally be adopted. The basic process is as follows:

1. The client of the third-party website goes to the authorization server and submits it ClientID,Redirect address RedirectUri And other information.
2. The user logs in to the authorization server and performs authorization approval (authorization approval can be configured to be completed automatically).
3. After authorization, redirect back to the redirection address provided by the client before, and attach the authorization code.
4. The third-party website server passes the authorization code +ClientID+ClientSecret Go to the authorization server Token. there Token Include access Token And refresh Token,visit Token Refresh with after expiration Token To get new access Token. 

5. Because we won't be exposed ClientSecret,Nor will it expose access to the outside world Token,
6. Use authorization code at the same time Token The process is carried out by the server,
7. The client only gets the one-time authorization code, so this mode is relatively safe.

 

Open redirection problem

When redirecting anonymous users to the login page, we usually bring redirectUrl, so that users can quickly return to the previous page after logging in. Hackers may forge an active link, consisting of a real website + phishing redirectUrl, and send an email to induce users to log in. Users actually visit the real website when logging in, so it is not easy to detect that redirectUrl is a phishing website. After logging in, they come to the phishing website, and users may unknowingly disclose important information.

1. The first is to fix the target of redirection URL. 
2. Second, you can specify the target of redirection by numbering URL,That is, the target of redirection URL It can only be on our white list.
3. Third, use reasonable and sufficient verification methods to verify the target address of the jump. If it is a non own address, inform the user that the jump is risky and beware of the threat of phishing websites.