目录
1. 代码质量:比较Locale相关的数据未指定适当的Locale
扫描工具
奇安信全卫士
高危
1. SQL注入
1.1. 预编译
JdbcTemplate,拼接SQL语句,如果有参数,尽量使用query(String sql, Object[] args)等方法,参数使用占位符?拼接,这些方法底层都使用了预编译。
Mybatis框架,$会引发SQL注入的风险。
#{}实质就是使用了预编译防止SQL注入,使用替换占位符?的方式,如果参数中存在注入到语句,也会被当作参数的一部分,而不会被执行。
select * from user where user_name = ${userName}
select * from user where user_name = #{userName}
如果userName参数为【"张三" or 1=1】,两种符号最终的SQL会不一样。
-- 最终会查出全量数据
select * from user where user_name = "张三" or 1=1
-- 查出user_name为【"张三" or 1=1】的user
select * from user where user_name = '"张三" or 1=1'
好处:
-
SQL会首先编译成数据库可以识别的语句,同时预编译一次,如果多次执行,节省编译时间。
-
SQL中参数使用占位符,防止SQL注入。
1.2. 参数校验
Mybatis框架,有些情况下无法使用#{},只能使用${},比如说参数是表名称、字段名称、备注等,无法使用#{},这种情况需要对参数进行校验,防止存在SQL注入的风险。
下面的正则会校验是否存在某些敏感操作的关键字,可根据情况适当增减。
static String reg = "(?:')|(?:--)|(/\\*(?:.|[\\n\\r])*?\\*/)|" + "(\\b(select|update|and|or|delete|insert|trancate|char|into|substr|ascii|declare|exec|count|master|into|drop|execute)\\b)";
1.3. SQL语句校验
如果获取到的是一个完整的SQL语句,那么也需要对完整的SQL语句做一个校验。
如下正则是校验一个查询语句中是否包含了其他敏感操作,可根据实际情况增减。
/**
* 是否为查询语句正则
*/
public static final String SQL_REX = "(?i)^(\\s*)(select)(\\s+)(((?!([\\s\\t\\n]|[(]+)(insert|delete|update|drop|alter|execute|declare|exec)(\\s+)).)+)$";
2. XSS漏洞
XSS跨站脚本攻击,指的是恶意攻击者往Web页面里插入恶意html代码。分为反射型XSS和存储型XSS。
实质的区别是反射型的风险来源是web请求的数据,存储型是数据库查出来的数据包含风险。
由此看来,如果从每个接口的入参出手,对每个参数进行校验、过滤、编码,那么就能从实质上解决反射型和存储型数据所附带的XSS风险操作。
实际拦截器并不能完全防止XSS,因为存储型的XSS,数据中存储的数据不仅仅通过web接口增加,所以其实想要完全避免存储型的漏洞,还需要对接口响应数据进行过滤。可以通过实现ResponseBodyAdvice<T>接口实现。
2.1 过滤器
在过滤器中,进行如下操作:
-
对特殊字符(如`<、>、'、"`以及`<script>、javascript`等进行过滤。
-
根据数据将要置于HTML上下文中的不同位置(HTML标签、HTML属性、JavaScript脚本、CSS、URL),对所有不可信数据进行恰当的输出编码。
-
设置HttpOnly属性,避免攻击者利用跨站脚本漏洞进行Cookie劫持攻击.
具体实现原理:
-
重写HttpServletRequestWrapper,重写getParameter(),getParameterValues(),getHeader(),getHeaders()在这几个方法中,对参数进行过滤。
-
创建过滤器覆盖原来的HttpServletRequest
-
Response设置HttpOnly属性,避免攻击者利用跨站脚本漏洞进行Cookie劫持攻击
相关代码
/**
* 过滤器
*/
@Slf4j
public class HttpRequestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@SneakyThrows
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Set-Cookie","cookiename=cookievalue; path=/; Domain=domainvaule; Max-age=seconds; HttpOnly");
ParamHttpServletRequestWrapper request = new ParamHttpServletRequestWrapper(httpServletRequest);
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
/**
* HttpServletRequestWrapper装饰类
*/
@Slf4j
public class ParamHttpServletRequestWrapper extends HttpServletRequestWrapper {
HttpServletRequest orgRequest = null;
private String currentUrl;
private Map<String, String[]> parameterMap;
private Map<String, String> headerMap;
public ParamHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
orgRequest = request;
this.setCurrentUrl(request.getRequestURI());
parameterMap = request.getParameterMap();
headerMap = new HashMap<>();
}
private void setCurrentUrl(String url){
this.currentUrl = url;
}
public String getCurrentUrl(){
return this.currentUrl;
}
/**
* 获取原始的request
* @return
*/
public HttpServletRequest getOrgRequest() {
return orgRequest;
}
/**
* 获取最原始的request的静态方法
* @return
*/
public static HttpServletRequest getOrgRequest(HttpServletRequest request) {
if (request instanceof ParamHttpServletRequestWrapper) {
return ((ParamHttpServletRequestWrapper) request).getOrgRequest();
}
return request;
}
/**
* 获取所有参数名
* @return 返回所有参数名
*/
@Override
public Enumeration<String> getParameterNames() {
Vector<String> vector = new Vector<>(parameterMap.keySet());
return vector.elements();
}
/**
* 覆盖getParameter方法,将参数名和参数值都做xss & sql过滤。<br/>
* 如果需要获得原始的值,则通过super.getParameterValues(name)来获取<br/>
* getParameterNames,getParameterValues和getParameterMap也可能需要覆盖
*/
@Override
public String getParameter(String name) {
String[] results = parameterMap.get(name);
if (results == null || results.length <= 0)
return null;
else {
String value = results[0];
return XssEncodeUtil.stripXSS(value);
}
}
/**
* 获取指定参数名的所有值的数组,如:checkbox的所有数据 接收数组变量 ,如checkobx类型
*/
@Override
public String[] getParameterValues(String name) {
String[] results = parameterMap.get(name);
if (results == null || results.length <= 0)
return null;
else {
int length = results.length;
for (int i = 0; i < length; i++) {
results[i] = XssEncodeUtil.stripXSS(results[i]);
}
return results;
}
}
public void addHeader(String name, String value) {
this.headerMap.put(name, value);
}
/**
* 覆盖getHeader方法,将参数名和参数值都做xss & sql过滤。<br/>
* 如果需要获得原始的值,则通过super.getHeaders(name)来获取<br/>
* getHeaderNames 也可能需要覆盖
*/
@Override
public String getHeader(String name) {
String headerValue = headerMap.get(name);
if (headerValue != null) {
return XssEncodeUtil.stripXSS(headerValue);
}
return XssEncodeUtil.stripXSS(super.getHeader(name));
}
@Override
public Enumeration<String> getHeaders(String name) {
ArrayList<String> values = Collections.list(super.getHeaders(name));
if (headerMap.containsKey(name)) {
values.add(headerMap.get(name));
}
return Collections.enumeration(values);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
return super.getInputStream();
}
}
/**
* ESAPI工具类,防止xss,SQL注入等漏洞
*/
public class XssEncodeUtil {
public static String encodeForHTML(String str){
return StringEscapeUtils.escapeHtml(str);
}
public static String encodeForJavaScript(String str){
return StringEscapeUtils.escapeJavaScript(str);
}
public static String encodeForSQLParam(String param){
return StringEscapeUtils.escapeSql(param);
}
/**
* 防止xss跨脚本攻击(ESAPI编码)
*/
public static String stripXSS(String value) {
if (StringUtils.isNotEmpty(value)) {
Pattern scriptPattern = Pattern.compile(
"<[\r\n| | ]*script[\r\n| | ]*>(.*?)</[\r\n| | ]*script[\r\n| | ]*>", Pattern.CASE_INSENSITIVE);
if(scriptPattern.matcher(value).find()){
return encodeForHTML(value);
}
scriptPattern = Pattern.compile("src[\r\n| | ]*=[\r\n| | ]*[\\\"|\\\'](.*?)[\\\"|\\\']",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
if(scriptPattern.matcher(value).find()){
return encodeForHTML(value);
}
// Remove any lonesome </script> tag
scriptPattern = Pattern.compile("</[\r\n| | ]*script[\r\n| | ]*>", Pattern.CASE_INSENSITIVE);
if(scriptPattern.matcher(value).find()){
return encodeForHTML(value);
}
// Remove any lonesome <script ...> tag
scriptPattern = Pattern.compile("<[\r\n| | ]*script(.*?)>",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
if(scriptPattern.matcher(value).find()){
return encodeForHTML(value);
}
// Avoid eval(...) expressions
scriptPattern = Pattern.compile("eval\\((.*?)\\)",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
if(scriptPattern.matcher(value).find()){
return encodeForHTML(value);
}
// Avoid e-xpression(...) expressions
scriptPattern = Pattern.compile("e-xpression\\((.*?)\\)",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
if(scriptPattern.matcher(value).find()){
return encodeForHTML(value);
}
// Avoid javascript:... expressions
scriptPattern = Pattern.compile("javascript[\r\n| | ]*:[\r\n| | ]*", Pattern.CASE_INSENSITIVE);
if(scriptPattern.matcher(value).find()){
return encodeForJavaScript(value);
}
// Avoid vbscript:... expressions
scriptPattern = Pattern.compile("vbscript[\r\n| | ]*:[\r\n| | ]*", Pattern.CASE_INSENSITIVE);
if(scriptPattern.matcher(value).find()){
return encodeForJavaScript(value);
}
// Avoid onload= expressions
scriptPattern = Pattern.compile("onload(.*?)=",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
if(scriptPattern.matcher(value).find()){
return encodeForHTML(value);
}
}
return value;
}
}
中危
1. 代码质量:比较Locale相关的数据未指定适当的Locale
String.toUpperCase()和String.toLowerCase()应指定相应的Locale。
将字符转换为大写字符。在英文Locale中,会将`title`转换为`TITLE`;在土耳其Locale中,会将`title`转换为`T?TLE`,其中的`?`是拉丁字母的`I`。
"aaa".toUpperCase(Locale.ENGLISH)
2. 代码质量:使用==或!=比较基本数据类型的包装类
不能直接使用`==`或`!=`操作符来比较的两个基本数据类型的包装类型的值,因为这些操作符比较的是对象的引用而不是对象的值。
不过由于Java的缓存机制,所以如果基本类型的包装类是一个整数且在-128和127之间,或是布尔类型true或false,或者是'\u0000'和'\u007f'之间的字符文本,可以使用`==`或`!=`进行比较。也就是说,如果使用了基本类型的包装类型(除去Boolean或Byte),则会缓存或记住一个值区间。对于值区间内的值,使用`==`或`!=`会返回正确的值,而对于值区间外的值,将返回对象地址的比较结果。
3. 输入验证:日志伪造
允许日志记录未经验证的用户输入,会导致日志伪造攻击。一般日志伪造出现的原因是,日志文件中存在一些修改linux系统的敏感操作的指令。这些指令一般都需要另起一行,所以一般包含换行符。所以对输出日志进行编码,即可防止此种情况发生。
//对字符串进行编码
StringEscapeUtils.escapeJavaScript(str);
4. 密码管理:配置文件中的明文密码
配置文件中采用明文存储密码,所有能够访问该文件的人都能访问该密码,将会降低系统安全性。
使用jasypt加密工具将配置文件中的敏感信息加密。
<dependency>
<groupId>org.jasypt</groupId>
<artifactId>jasypt</artifactId>
<version>需要依赖的版本</version>
</dependency>
5. 密码管理:硬编码加密密钥
常量属性名避免带key,使用的话最好将值进行加密。
6. 资源管理:资源未释放:流
程序创建或分配流资源后,不进行合理释放,将会降低系统性能。攻击者可能会通过耗尽资源池的方式发起拒绝服务攻击。
//jdk1.8之后提供,try(){}可以自动关闭资源,防止忘记关闭资源
try (InputStream inputStream = new FileInputStream("file")){
}catch(){
}
7. 敏感信息泄漏
常量的参数名尽量不要使用username、password、pwd、addressee、name等敏感字段,会被漏扫工具监测到敏感信息泄漏。如果存在的话,需要对字段的值进行加密,在使用的时候进行解密。