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("expression\\((.*?)\\)", 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; }