Implementation of springboot web application security policy

Posted by dcf1023 on Sun, 05 Dec 2021 00:50:33 +0100

background

Recently, the project has been launched, and Party A requires to pass the safety inspection before acceptance. Therefore, a series of safety reinforcement has been carried out for the system according to the scanning results. This paper introduces some common safety problems and protection strategies, and provides corresponding solutions

Cross site scripting attack

XSS often occurs in forum comments and other systems. Now the rich text editor has protected XSS, but we still need to filter data on the back-end interface,

The common protection strategy is to filter and replace the malicious submitted scripts through filters

public class XSSFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        //System.out.println("XSSFilter");
        String contentType = request.getContentType();
        if (StringUtils.isNotBlank(contentType) && contentType.contains("application/json")) {
            XSSBodyRequestWrapper xssBodyRequestWrapper = new XSSBodyRequestWrapper((HttpServletRequest) request);
            chain.doFilter(xssBodyRequestWrapper, response);
        } else {
            chain.doFilter(request, response);
        }
    }
}
public class XSSBodyRequestWrapper extends HttpServletRequestWrapper {

    private String body;

    public XSSBodyRequestWrapper(HttpServletRequest request) {
        super(request);
        try{
            body = XSSScriptUtil.handleString(CommonUtil.getBodyString(request));
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {

        final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes(Charset.forName("UTF-8")));

        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return bais.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        };
    }

}
public class XSSScriptUtil {
    public static String handleString(String value) {
        if (value != null) {
            Pattern scriptPattern = Pattern.compile("<script>(\\s*.*?)</script>",
                    Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("-");
            scriptPattern = Pattern.compile("</script(\\s*.*?)>",
                    Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("-");
            scriptPattern = Pattern.compile("<script(\\s*.*?)>",
                    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE
                            | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("-");
            scriptPattern = Pattern.compile("eval\\((.*?)\\)",
                    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE
                            | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("-");
            scriptPattern = Pattern.compile("e­xpression\\((.*?)\\)",
                    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE
                            | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("-");
            scriptPattern = Pattern.compile("javascript:",
                    Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("-");
            scriptPattern = Pattern.compile("vbscript:",
                    Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("-");
            scriptPattern = Pattern.compile("onload(.*?)=",
                    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE
                            | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("-");


            scriptPattern = Pattern.compile("<+.*(oncontrolselect|oncopy|oncut|ondataavailable|ondatasetchanged|ondatasetcomplete|ondblclick|ondeactivate|ondrag|ondragend|ondragenter|ondragleave|ondragover|ondragstart|ondrop|onerror|onerroupdate|onfilterchange|onfinish|onfocus|onfocusin|onfocusout|onhelp|onkeydown|onkeypress|onkeyup|onlayoutcomplete|onload|onlosecapture|onmousedown|onmouseenter|onmouseleave|onmousemove|onmousout|onmouseover|onmouseup|onmousewheel|onmove|onmoveend|onmovestart|onabort|onactivate|onafterprint|onafterupdate|onbefore|onbeforeactivate|onbeforecopy|onbeforecut|onbeforedeactivate|onbeforeeditocus|onbeforepaste|onbeforeprint|onbeforeunload|onbeforeupdate|onblur|onbounce|oncellchange|onchange|onclick|oncontextmenu|onpaste|onpropertychange|onreadystatechange|onreset|onresize|onresizend|onresizestart|onrowenter|onrowexit|onrowsdelete|onrowsinserted|onscroll|onselect|onselectionchange|onselectstart|onstart|onstop|onsubmit|onunload)+.*=+",
                    Pattern.CASE_INSENSITIVE | Pattern.MULTILINE
                            | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("-");



            // Filter emoji expressions
            scriptPattern = Pattern
                    .compile(
                            "[\ud83c\udc00-\ud83c\udfff]|[\ud83d\udc00-\ud83d\udfff]|[\u2600-\u27ff]",
                            Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("-");
        }
        return value;
    }
}

SQL injection

sql injection is one of the most common security problems in the system, which will lead to login security, data access rights security, etc. in addition to maintaining parametric writing of sql statements, we also need to use interceptors to detect and submit parameters and give error prompts when sensitive characters appear

@Component
public class SQLInjectInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //System.out.println("SQLInjectInterceptor");
        boolean isvalid = true;
        String contentType = request.getContentType();
        if (StringUtils.isNotBlank(contentType) && contentType.contains("application/json")) {
            String body = CommonUtil.getBodyString(request);
            try {
                Object object = JSON.parse(body);
                if (object instanceof JSONObject) {
                    JSONObject jsonObject = JSONObject.parseObject(body);
                    for (Map.Entry<String, Object> item : jsonObject.entrySet()) {
                        String value = ConvertOp.convert2String(item.getValue());
                        if (SQLInjectUtil.checkSQLInject(value)) {
                            isvalid = false;
                            break;
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (!isvalid) {
            response.sendRedirect(request.getContextPath() + "/frame/error/sqlInjectionError");
        }
        return isvalid;
    }

}
public class SQLInjectUtil {
    public static String keyWord = "select|update|delete|insert|truncate|declare|cast|xp_cmdshell|count|char|length|sleep|master|mid|and|or";

    public static boolean checkSQLInject(String value) {
        boolean flag = false;
        value = ConvertOp.convert2String(value).toLowerCase().trim();
        if (!StringUtil.isEmpty(value) && !StringUtil.checkIsOnlyContainCharAndNum(value)) {
            List<String> keyWordList = Arrays.asList(keyWord.split("\\|"));
            for (String ss : keyWordList) {
                if (value.contains(ss)) {
                    if (StringUtil.checkFlowChar(value, ss, " ", true) ||
                            StringUtil.checkFlowChar(value, ss, "(", true) ||
                            StringUtil.checkFlowChar(value, ss, CommonUtil.getNewLine(), true)) {
                        flag = true;
                        break;
                    }
                }
            }
        }
        return flag;
    }
}

HTTP request method restrictions

We should only keep the request methods required by the system. Other methods, such as DELETE, PUT, TRACE, etc., will cause system data leakage or damage. Generally, it can be configured in the running container. For projects running jar packages, because the built-in tomcat is used, separate configuration file code is required to control them

@Configuration
public class TomcatConfig {
    @Bean
    public TomcatServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcatServletContainerFactory = new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint constraint = new SecurityConstraint();
                SecurityCollection collection = new SecurityCollection();
                //http method
                List<String> forbiddenList = Arrays.asList("PUT|DELETE|HEAD|TRACE".split("\\|"));
                for (String method:forbiddenList) {
                    collection.addMethod(method);
                }
                //url matching expression
                collection.addPattern("/*");
                constraint.addCollection(collection);
                constraint.setAuthConstraint(true);
                context.addConstraint(constraint );
                //Set to use httpOnly
                context.setUseHttpOnly(true);
            }
        };
        tomcatServletContainerFactory.addConnectorCustomizers(connector -> {
            connector.setAllowTrace(true);
        });
        return tomcatServletContainerFactory;
    }

}

User rights

Password encryption

MD5 algorithm is commonly used to store user passwords to ensure data security. Because the encryption results of MD5 are fixed, we need to add salt during encryption to ensure the uniqueness of each password. We use MD5 (password + "|" + login name) and improve the processing when the encrypted content is in Chinese, Avoid inconsistent MD5 encryption results at the front and rear ends

public class EncryptUtil {
    public static String encryptByMD5(String str) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        //Generate md5 encryption algorithm
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        md5.update(str.getBytes("UTF-8"));
        byte b[] = md5.digest();
        int i;
        StringBuffer buf = new StringBuffer("");
        for (int j = 0; j < b.length; j++) {
            i = b[j];
            if (i < 0)
                i += 256;
            if (i < 16)
                buf.append("0");
            buf.append(Integer.toHexString(i));
        }
        String md5_32 = buf.toString();		//The 32-bit encryption is consistent with the MD5 function of mysql.
//        String md5_16 = buf.toString().substring(8, 24); 	// 16 bit encryption
        return md5_32;
    }
}

Login verification code

The login verification code is implemented based on redis, and the traditional session implementation method will be limited in the case of cross domain of higher version of chrome

The implementation method of verification code is to generate random characters, generate corresponding Base64 pictures according to the random characters, return the pictures to the front end, store the characters in Redis and set the expiration time

@Component
public class ValidateCodeUtil {
    private static Random random = new Random();
    private int width = 165; //Width of verification code
    private int height = 45; //Verification code high
    private int lineSize = 30; //Number of interference lines included in the verification code
    private int randomStrNum = 4; //Number of verification code characters

    private String randomString = "0123456789";
    private final String sessionKey = "ValidateCode";

    private int validDBIndex = 2;
    @Autowired
    RedisUtil redisUtil;

    @Autowired
    private FrameConfig frameConfig;

    public String getBase64ValidateImage(String key) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // BufferedImage class is an image class with buffer. Image class is a class used to describe image information
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
        Graphics g = image.getGraphics();
        g.fillRect(0, 0, width, height);
        g.setColor(getRandomColor(105, 189));
        g.setFont(getFont());
        //Interference line
        for (int i = 0; i < lineSize; i++) {
            drawLine(g);
        }

        //Random character
        String randomStr = "";
        for (int i = 0; i < randomStrNum; i++) {
            randomStr = drawString(g, randomStr, i);
        }
        g.dispose();
        redisUtil.redisTemplateSetForList(key,sessionKey,randomStr,validDBIndex);
        redisUtil.setExpire(key, frameConfig.getValidatecode_expireseconds(),TimeUnit.SECONDS,validDBIndex);
        String base64String = "";
        try {
            //  Return picture directly
            //  ImageIO.write(image, "PNG", response.getOutputStream());
            //Return base64
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ImageIO.write(image, "PNG", bos);
            byte[] bytes = bos.toByteArray();
            Base64.Encoder encoder = Base64.getEncoder();
            base64String = encoder.encodeToString(bytes);

        } catch (Exception e) {
            e.printStackTrace();
        }

        return base64String;
    }

    public String checkValidate(String key,String code){
        String errorMessage = "";
        if(redisUtil.isValid(key,validDBIndex)){
            String sessionCode = ConvertOp.convert2String(redisUtil.redisTemplateGetForList(key,sessionKey,validDBIndex));
            if(!code.toLowerCase().equals(sessionCode)){
                errorMessage = "Incorrect verification code";
            }
        }else{
            errorMessage = "Verification code has expired";
        }
        return errorMessage;
    }

    //Color settings
    private  Color getRandomColor(int fc, int bc) {
        fc = Math.min(fc, 255);
        bc = Math.min(bc, 255);
        int r = fc + random.nextInt(bc - fc - 16);
        int g = fc + random.nextInt(bc - fc - 14);
        int b = fc + random.nextInt(bc - fc - 12);

        return new Color(r, g, b);
    }

    //Font settings
    private Font getFont() {
        return new Font("Times New Roman", Font.ROMAN_BASELINE, 40);
    }

    //Drawing of interference line
    private void drawLine(Graphics g) {
        int x = random.nextInt(width);
        int y = random.nextInt(height);
        int xl = random.nextInt(20);
        int yl = random.nextInt(10);
        g.drawLine(x, y, x + xl, y + yl);

    }
    //Acquisition of random characters
    private  String getRandomString(int num){
        num = num > 0 ? num : randomString.length();
        return String.valueOf(randomString.charAt(random.nextInt(num)));
    }
    //String drawing
    private String drawString(Graphics g, String randomStr, int i) {
        g.setFont(getFont());
        g.setColor(getRandomColor(108, 190));
        //System.out.println(random.nextInt(randomString.length()));
        String rand = getRandomString(random.nextInt(randomString.length()));
        randomStr += rand;
        g.translate(random.nextInt(3), random.nextInt(6));
        g.drawString(rand, 40 * i + 10, 25);
        return randomStr;
    }
}

Kick people off the line

This function ensures that a user account can only log in on the same device of the same type. If different devices log in repeatedly, other login machines will log in automatically. Therefore, we need to store the user's login status. The table structure is designed as follows. LoginFrom identifies the login source, such as computer, mobile terminal, large screen machine, etc. websoket can be used for automatic offline operation

CREATE TABLE `f_online` (
  `UnitGuid` varchar(50) NOT NULL,
  `UserGuid` varchar(50) DEFAULT NULL,
  `UserName` varchar(100) DEFAULT NULL,
  `LoginFrom` varchar(50) DEFAULT NULL,
  `LoginDate` datetime DEFAULT NULL,
  `LoginToken` varchar(100) DEFAULT NULL,
  `ReserveA` varchar(100) DEFAULT NULL,
  `ReserveB` varchar(100) DEFAULT NULL,
  `ReserveC` varchar(100) DEFAULT NULL,
  `ReserveD` varchar(100) DEFAULT NULL,
  `SpareX` varchar(100) DEFAULT NULL,
  `SpareY` varchar(100) DEFAULT NULL,
  `SpareZ` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`UnitGuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


Login error lock

In order to avoid malicious attempts at password login, we need to temporarily lock users who log in incorrectly within a certain period of time. Combined with the login log, for example, if the login fails for more than 5 times within 1 minute, we will lock the account for 1 minute, generate and store the locked key in redis according to the user name, and set the locking time, First check whether there is a corresponding lock when logging in next time

Druid settings

When the system integrates Druid thread pool, the monitoring page will be exposed by default. We should set the login permission to avoid database information disclosure

    @Bean
    public ServletRegistrationBean druidServlet() {
        ServletRegistrationBean reg = new ServletRegistrationBean();
        reg.setServlet(new StatViewServlet());
        reg.addUrlMappings("/druid/*");
        reg.addInitParameter("allow", ""); //White list
        reg.addInitParameter("loginUsername", "admin");
        reg.addInitParameter("loginPassword", "11111");
        return reg;
    }

Topics: Java Spring Boot Web Security