java代码审计规范
目录
1 身份认证安全... 2
1.1登录入口... 2
1.1.1密码强度策略... 2
1.1.2验证码生成... 2
1.2.3验证码后端校验... 4
1.2.4认证错误限制锁定... 4
1.2.5短信验证码:... 5
1.2会话安全... 6
1.2.1cookie管理... 6
1.2.2 session管理... 6
1.2.3会话重放... 7
2.权限控制—越权漏洞... 8
2.1水平越权:... 8
2.2垂直越权:... 9
3.数据交互安全... 11
3.1跨站脚本攻击(xss)... 11
3.2sql注入... 13
3.3命令注入... 15
3.4 xml实体注入... 16
3.5文件下载:... 16
3.6文件上传... 18
3.7跨站请求伪造(csrf)... 20
4.组件安全... 22
4.1 fastjson. 22
4.2 jackson-databind:... 22
4.3 shiro:... 22
4.4 struts2:... 22
1 身份认证安全
1.1登录入口
1.1.1密码强度策略
介绍:密码是验证的一种方式,当用户密码过于简单,会造成弱口令,导致用户信息等敏感信息泄露。
审计策略:通过定位登录入口,查看是否对密码复杂度进行判断
代码规范示例:(此代码进行口令复杂度校验,长度大于八位,含有数字,大小写字母,符号)
function checkPass(pass){
if(s.length < 8){
return 0;}
var ls = 0;
if(s.match(/([0-9])+/)||s.match(/([A-Z])+/)||s.match(/[^a-zA-Z0-9]+/)){ //【正则表达式】
ls++;
}
return ls;
}
if(checkPass(form.password.value)<3){
alert("密码复杂度不够,请重新设置!");
form.password.focus();
return false ;
}
代码规范示例(弱口令检测)
String pass=request.getParameter("password");
// 对用户口令基于基本密码策略+弱口令进行检测
Map<String,String> map = CheckPassword.isComplex(user, pass, 1);
if(!map.get("returnCode").equals("00")){
String message = map.get("returnMsg");
out.write(message);
response.sendRedirect("Register.jsp?err="+message);
return;
}
1.1.2验证码生成
介绍:验证码是保护用户信息安全的一种方式,如果验证码校验简单,未使用复杂,有干扰的验证码,攻击者会使用工具进行爆破,绕过验证码校验。
审计策略:定位登录入口,随机函数是random。可通过关键函数搜索。
代码规范示例:(产生随机四位验证码,其中包含数字字母大小写,并且有干扰线)
int charNum = 4; // 随机产生字符数量
if(num != null){
charNum = Integer.parseInt(num);
}
String randString=""; //需要绘制的随机字符串
// BufferedImage 类描述具有可访问图像数据缓冲区的 Image
BufferedImage buffImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
Graphics2D g = buffImage.createGraphics()
//设置验证码尺寸、背景颜色、字体
//绘制干扰线
/产生随机/字符集
//绘制干扰线
int lineSize = 100; // 干扰线数量
Random random = new Random();
for (int i = 0; i <= lineSize; i++)
{
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(width/8);
int yl = random.nextInt(height/8);
g.setColor(randColor(130, 250));
g.drawLine(x, y, x + xl, y + yl);
}
//产生随机验证码
char[] characterSet ={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',' Q','R','S','T','U','V','W','X','Y','Z'};
for (int i = 1; i <= charNum; i++){
g.setColor(randColor(20,130));
String rand =String.valueOf(characterSet[random.nextInt(characterSet.length)]); // 获取随机的字符
g.translate(random.nextInt(3), random.nextInt(3)); //随机文字宽度
g.drawString(rand, width/(charNum+2) * i, height/4*3) //绘制位置
randString += rand; //服务器存储验证码
}
session.setAttribute("validateCode", randString); //服务器存储验证码
//禁止图像缓存
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
1.2.3验证码后端校验
介绍:验证码不适用后端校验,攻击者可在前端删除校验代码,达到绕过校验,后端校验也需要设置验证码时效,验证码使用一次应及时销毁。
审计策略:定位登录入口,进行查看。
代码规范示例:(清空验证码或者重写验证码,一次一验)
String captcha = request.getParameter("captcha").trim();
HttpSession session = request.getSession();
String imgCode = (String) session.getAttribute("sercurity.captcha");
if (captcha ! = null && !captcha.equals("") && captcha.equalsIgnoreCase(imgCode)) {
//验证码为真
// 清空验证码或重写验证码
session.setAttribute("sercurity.captcha", null);
//…………省略代码………………账户密码校验过程
response.sendRedirect("index.jsp");
return;
}else{ //验证码为假
// 清空验证码或重写验证码
session.setAttribute("sercurity.captcha", null);
response.sendRedirect("login.jsp?err=captcha_wrong");
return;
}
1.2.4认证错误限制锁定
介绍:用户认证时间间隔不做限制,攻击者可无限爆破。
审计策略:定位登录入口,查看是否对认证时间做限制。
代码规范示例:(对认证次数以及登录相隔时间做了限制)
if(!logonDao.getIsRightPassword(un.getPassword())){
// 判断错误次数及登录相隔时间
long diffMin=(new Date().getTime()-loginLockTime.getTime())/(1000 * 60); // 相隔的时间
if(true == isLock){
if(diffMin > 20){isLock = false;} // 间隔 20 分钟之后,解锁账户
else{throw new Exception("您在 3 分钟内登录错误达到 5 次,20 分钟后再试");}
}
if(diffMin > 3){ // 间隔大于 3 分钟,清空计数器
loginLockTime = new Date();
failCount = 0;
}
failCount++;
if(failCount >= 5){
isLock = true;
throw new HandlerException("您在 3 分钟内登录错误达到 5 次,20 分钟后再试");
}
} // isLock 关联 IP 和账户
1.2.5短信验证码:
介绍:短信验证码是校验用户是否正确的一种方式,如果对发送短信的时间不做限制,攻击者可以通过脚本执行发送短信,进行短信轰炸,消耗流量。
审计策略:定位登录入口,查看是否对验证码发送时间以及间隔时间做校验。
安全规范代码:(手机号后端获取,发送次数校验防止轰炸)
if(System.currentTimeMillis()-60000<Long.parseLong(sms_time)){ // 短信验证码发送间隔检查
priDataCache.setParam(“respmsg”, “短信验证码已发送,一分钟后可重新发送");
return -1;
}
String tel=(String)request.getSession().getAttribute(”tel“); //后端获取发送的手机号码
(String)request.getSession().setAttribute(“sms_time”, TimeStamp); //标记发送时间
安全规范代码:(短信提交验证,时效性,错误次数验证防止爆破)
if(System.currentTimeMillis()-60000>Long.parseLong(sms_time)){ // 短信验证码超时检查
priDataCache.setParam(“sms_yzm”, “”); // 超时清空验证码
priDataCache.setParam("sms_yzm_time", "");
priDataCache.setParam("respmsg", "短信验证码已经超时,请重新获取");
return -1;
}
if(sms_input!=null&&sms_input.equals(sms_yzm)){ // 记录短信验证码使用并清空
priDataCache.setParam("sms_yzm", "");
priDataCache.setParam("sms_yzm_time", "");
return 1;
}
if(sms_input!=null){ // 记录错误次数,判断是否超限
int msgCount = iBaseDao.queryForInt("customer.countMsg", param);
if(msgCount>5){isBeyondCount = true;}
if(isBeyondCount){
TransUtil.responseMessage
(AppConstants.RspCode_FAIL, "错误次数过多!",rst);
// return rst 表示存在爆破,调用锁定逻辑,20 分钟之后再尝试
return rst;
1.2会话安全
1.2.1cookie管理
介绍:cookie是存储在用户本地的,是文本文件。比如一些浏览器的登录,会记录用户密码,账号,从而在下次登录时,不在进行校验,攻击者利用人的惰性,劫持用户cookie,伪造用户身份进行登录。从而产生危害。
审计策略:搜索cookie,httponly,secureflag关键字,查看是否有有效时间,是否添加对抗xss脚本。
安全代码示例:
Cookie cookie = new Cookie(map);
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);
response.setHeader("x-xss-protection", "1; mode=block ");
Spring Security 动态添加 Security Headers:
applicationContext-security.xml:
<http>
<!-- ... -->
<headers>
<xss-protection />
</headers>
</http>
1.2.2 session管理
介绍:session是存储在服务器的,在每次执行操作是都进行一次验证,看是否具有操作当前数据的权限。用户的登录状态,登录前后,会话有效期,会话超时,都会进行刷新验证。
审计策略:搜索关键字session。看是否在更新会话是移除旧会话。
代码规范示例(对会话进行移除刷新)
public HttpSession changeSessionIdentifier(HttpServletRequest request) throws Exception {
HttpSession oldSession = request.getSession();
Map<String,Object> temp = new ConcurrentHashMap<String,Object>();
Enumeration e = oldSession.getAttributeNames();
while (e != null && e.hasMoreElements()) { // 复制会话内容
String name = (String) e.nextElement();
Object value = oldSession.getAttribute(name);
temp.put(name, value);
}
oldSession.invalidate();
HttpSession newSession = request.getSession();
User user = ESAPI.authenticator().getCurrentUser();
user.addSession( newSession );// 更新会话
user.removeSession( oldSession );// 更新会话一定要移除旧会话
// 写入会话内容
for (Map.Entry<String, Object> stringObjectEntry : temp.entrySet()) newSession.setAttribute(stringObjectEntry.getKey(),stringObjectEntry.getValue());
return newSession;
}
1.2.3会话重放
介绍:会话重放是指攻击者发送一个目的主机已接受过的包,达到欺骗系统的目的。
审计策略:定位登录入口,看是否对交互序列号进行判断。
安全规范代码:(对会话时效性进行判断是否为第一次交互,若是创建序列号,存入session告知客户端,若不是改变交易转态进行中,业务操作进行中,清空标识,改变状态)
String transid = request.getParameter("transid");
if(1!==Dao.queryStat(transid)){ // 1:操作可执行 0:操作已完成 -1:操作正在进行 rst.TransUtil.responseErrorMessage(CHECKMSG.OPERTED_ERROR);
return rst;
}else{
String timestamp = (String) session.getAttribute("timestamp");
Map<String, Object> rst = new HashMap<String, Object>();
if (null==timestamp||timestamp.equals("")) {
Date myTimeStamp = new Date().toString();
// 第一次交互在服务端生成时间戳作为标识存入 session
session.setAttribute("timestamp", myTimeStamp);
//时间戳(时效性),唯一ID标记会话(唯一性)
rst.put(“transid”, myTimeStamp);
return rst;
}else
{
String serialNum = (String) request.getParameter("serialNum");
// 第二次交互检测标识并完成交易,并在交易完成后清空标识
if (null != serialNum && serialNum == timestamp) {
CT.changeTransStat(transid ,-1); // 改变操作状态
if(transaction(transid)){
session.removeAttribute("timestamp");
CT.changeTransStat(transid,0);// 改变操作状态
}
return rst;
}
rst= TransUtil.responseErrorMessage(CHECKMSG.CHECK_FAIL);
return rst;
}
2.权限控制—越权漏洞
介绍:服务器端处理请求时,没有判断数据所属人,或判断操作执行权限,用户提交的 request 参数(用户可控),成为了数据获取或操作执行的主键,导致攻击者可以通过变换键值,访问了他人数据,或执行高权限操作。
常见越权:修改金额,数量,通过用户id请求用户信息等。
2.1水平越权:
介绍:同一级别,利用自己身份可以访问或修改另一用户信息的操作。
审计策略:在处理用户操作请求时查看是否有对当前登陆用户权限做校验从而确定是否存在漏洞。
漏洞示例:(通过修改请求中的userid,可以删除任意用户账单)
userID=Integer.valueOf( request.getParameter("userid"));
String orderID=request.getParameter("orderid");
JdbcConnection conn = null;
try {
conn = new JdbcConnection();
Object[] params = new Object[2];
params[0] = userID;
params[1] = orderID;
String sql = null;
sql = “delete from user where orderid=? and userid=?”;
Dao.updateUser(sql,params);
}catch{
}
代码规范示例:(通过session获取UserId,删除账单前判断用户是否具有删除该账单的权限)
public Object remove(Long addrID){
Map<String, Object> respMap = new HashMap<String, Object>();
if (orderID.isBelong(=(String)request.getSession().getAttribute(”userID"))) {
// 判断用户提交的数据是否属于当前登录用户
this.addressService.removeUserAddress(orderID, (String)request.getSession().getAttribute(”userID"));
respMap.put(Constants.RESP_STATUS_CODE_KEY, Constants.RESP_STATUS_CODE_SUCCESS);
respMap.put(Constants.MESSAGE,"地址删除成功!");
}else{
respMap.put(Constants.RESP_STATUS_CODE_KEY, Constants.RESP_STATUS_CODE_FAIL);
respMap.put(Constants.ERROR,"用户未登录,删除地址失败!");
}
return respMap;
}
2.2垂直越权:
介绍:低用户可以访问高用户的信息。执行高用户权限。
审计策越:在处理用户操作请求时查看是否有对当前登陆用户权限做校验从而确定是否存在漏洞。
漏洞示例:(没有进行身份验证普通用户构造deleteUser请求,通过userId可以删除任意用户)
@RequestMapping(value = "deleteUser")
public String deleteUser(HttpServletRequest request)throws Exception {
int id = request.getParameter("userid");
try {
userManager.delete(id);
request.setAttribute("msg", "删除用户成功");
} catch (ServiceException e) {
// logger.error(e.getMessage(), e);
request.setAttribute("msg", "删除用户失败");
}
return list(request);
}
代码规范示例:(对当前用户权限进行校验,通过sessionid判断用户角色)
public class PrivilegeFilter implements Filter{
public void init(FilterConfig config) throws ServletException{
// 获取资源访问权限配置
String fileName=config.getInitParameter("privilegeFile");
String realPath=config.getServletContext().getRealPath(fileName);
try{
properties.load(new FileInputStream(realPath));
}
catch(Exception e){
config.getServletContext().log("读取权限控制文件失败",e);
}
}
}
代码规范示例(web.xml中配置过滤器权限)
将权限访问规则存入 privilege.properties 文件中:
admin.do?action=* = super
list.do?action=add = admin
list.do?action=view = guest
Web.xml 中配置过滤器权限:
<filter>
<filter-name>privilegeFilter</filter-name>
<filter-class>com.filter.privilegeFilter</filter-class>
<init-param>
<param-name>privilegeFile</param-name>
<param-value>/WEB-INF/privilege.properties</param-value>
</init-param>
</filter>
代码规范示例(基于 URL 和 Method 访问控制)
Spring Security 提供了基于 URL 和 Method 的访问控制:
<sec:http>
<sec:intercept-url pattern="/super_portal.do**" access="ROLE_SUPER" />
<sec:intercept-url pattern="/manager_portal.do**" access="ROLE_MANAGER" />
<sec:intercept-url pattern="/**" access="ROLE_USER" />
<sec:form-login />
<sec:logout />
</sec:http>
代码规范示例(通过sessionid判断用户角色)
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws Exception{
String requestUri=request.getRequestURI().replace(request.getContextPath()+"/", "");
String action=request.getParameter("action");action=action==null?"":action;
String uri=requestUri+"?action="+action;
String role=(String)request.getSession().getAttribute("role");
role=role==null?"guest":role;
boolean authen=false;
for(Object obj:properties.keySet()){
String key=(String)obj;
if(uri.matches(key.replace("?", "\\?").replace(".", "\\.").replace("*", ".*"))){
//判断用户是否具有资源的访问权限
if(role.equals(properties.get(key))){authen=true;break;}
}
}
if(!authen){throw new RuntimeException("您无权访问该页面,请以合适的身份登录后查看。");}
chain.doFilter(request, response);
}
3.数据交互安全
3.1跨站脚本攻击(xss)
介绍: 由于程序对用户输入的内容未做过滤,导致攻击者构造的代码在浏览器解析执行,产生危害,xss可以进行木马钓鱼,劫持cookie,键盘记录等危害。
审计策略:扫描所有的HttpServletRequest 查看相关的上下文环境。
漏洞示例:(未对输入进行过滤,校验,攻击者可输入攻击代码)
<% out.print(request.getParameter("param")); %>
<script>
function test(){
var
str=document.getElementById(“text”).value;
document.getElementById(“t”).innerHTML=“<a
href=‘”+str+“’>test</a>";
}
</script>
<div
id="t"></div>
<input type = "text" id
="text" value="" />
<input type = "button"
id="s" value="write" onclick="test()" />
}
代码规范示例:(对用户输入进行循环过滤,对输出进行html转义,)
String btype = ce.XssFliter(request.getParameter(”ip"));
Public String XssFliter (String xss){
Properties prop = System.getProperties();
String os = prop.getProperty("os.name");
String[] vulnchars = {“|”,“;”,“&”,“$”, ”%“, ”@“,”>“,”<“,”`“,”\\“,”\””,”)”,”(”,”+”, ”,”,”,” \r”,};
String[] replacechar = null;
String xsss = xss.tochar();
Boolean needFilter=true;//用以标记
while(needFilter){
for(int i =0;i<xsss.length;i++)
// 遍历替换,
//如没有可替换的字符,则设置needFilter为false,不再循环
}
return xsss.toString();
}
代码规范示例(输出编码)
HTML 实体\属性编码:
<div>ESAPI.encoder().encodeForHTML($user.name)</div>
<div attr ="\""+ESAPI.encoder().encodeForHTMLAttribute($user.name)"\""/>
JavaScript 编码:
<script>alert('ESAPI.encoder().encodeForJavaScript ($user.name)')</script>
<script>x=\'ESAPI.encoder().encodeForJavaScript ($user.name)\'</script>
<div onmouseover="x='ESAPI.encoder().encodeForJavaScript ($user.name)'"/>
CSS 编码:
String safe = ESAPI.encoder().encodeForCSS($user.name));
public class HTMLEntityCodec extends AbstractIntegerCodec
{
……
代码规范示例(输出编码)
public String encode(char[] immune, String input)
{
StringBuilder sb = new StringBuilder();//新建字符序列
for (int offset = 0; offset < input.length();)
{
int point = input.codePointAt(offset);//使用Unicode代码点进行匹配
if (Character.isValidCodePoint(point)) {
sb.append(encodeCharacter(immune, point));
}
offset += Character.charCount(point);
}
return sb.toString();
}
代码规范示例(输出编码)
public String encodeCharacter(char[] immune, int codePoint)
{
if ((containsCharacter((char)codePoint, immune)) && (Character.isValidCodePoint(codePoint))) {
return new StringBuilder().appendCodePoint(codePoint).toString();
}
String hex = super.getHexForNonAlphanumeric(codePoint);
if ((hex == null) && (Character.isValidCodePoint(codePoint))) {
return new StringBuilder().appendCodePoint(codePoint).toString();
}
if (((codePoint <= 31) && (codePoint != 9) && (codePoint != 10) && (codePoint != 13)) || ((codePoint >= 127) && (codePoint <= 159)))
{
hex = "fffd";
codePoint = 65533;
}
String entityName = (String)characterToEntityMap.get(Integer.valueOf(codePoint));
if (entityName != null) {
return "&" + entityName + ";";
}
return "&#x" + hex + ";";
}
3.2sql注入
介绍:程序员对用户输入的内容未做过滤,攻击者编写sql语句带入到数据库查询操作,产生危害。
审计策略:这种一般可以直接黑盒找到,如果只是代码片段快速扫描可控制的参数或者相关的sql关键字查看。查看预编译的完整性,关键函数定位setObject()、setInt()、setString()、setSQLXML()关联上下文搜索set* 开头的函数。
漏洞示例:(未对输入内容进行过滤)
select *
from users
where name=‘$UserName' and password=‘$Password ’
安全规范代码:(对sql常见的字符进行过滤处理)
String btype = ipAdd.SqlFliter(request.getParameter(”ip"));
Public String SqlFliter(String sql){
Properties prop = System.getProperties();
String os = prop.getProperty("os.name");
String[] vulnchars = {“|”,“;”,“&”,“\’", “\”",">","<","`",”=","!"};
String[] vulnwords = {“ net user”,” schema”,… …};
String[] replacechar = null;
String sqls = sql.tochar();
Boolean needFilter=true;//用以标记
while(needFilter){
for(int i =0;i<sqls.length;i++)
// 遍历替换,如没有可替换的字符,则设置needFilter为false,不再循环
}
return sqls.toString();
}
代码规范示例(编码转义,特殊字符转义:)
private String encodeCharacterMySQL( Character c ) {
char ch = c.charValue();
if ( ch == 0x00 ) return “\\0”; //null
if ( ch == 0x08 ) return "\\b"; //BS
if ( ch == 0x09 ) return "\\t"; //TAB
if ( ch == 0x0a ) return "\\n"; //LF
if ( ch == 0x0d ) return "\\r"; //CR
if ( ch == 0x1a ) return "\\Z"; //SUB
if ( ch == 0x22 ) return “\\\”“; //双引号
if ( ch == 0x25 ) return “\\%”; //百分号
if ( ch == 0x27 ) return “\\’”; //单引号
if ( ch == 0x5c ) return “\\\\”; //反斜杠
if ( ch == 0x5f ) return “\\_”; //下划线
return "\\" + c;
}
代码规范示例(JDBC预编译:)
String custname = request.getParameter("customerName");
String query = "SELECT balance FROM user_data WHERE user_name = ? ";
PreparedStatement pstmt = connection.prepareStatement( query );
pstmt.setString( 1, custname); // 通过 setString 传递参数,PreparedStatment 会对传入的参数值做转义处理
ResultSet results = pstmt.executeQuery( );
Hibernate参数位置绑定:
Query query=session.createQuery(“from User user where user.name=? ”);
query.setString(0, userName);
Mybatis使用#进行:
<!—使用 #{id} 的传参方式,MyBatis 框架可对传入的参数值进行,对于不同的数据库语言有差异,但处理形式相同-->
MySQL:select * from table where name like concat(‘%’,#{name},’%’)
SQL Server:select * from table where name like ‘%’+#{name}+’%’
Oracle:select * from table where name like ‘%’ || #{name} || ’%’
DB2:select * from table where name like concat(‘%’,#{name},’%’)
代码规范示例(编码转义:引号逃逸法:)
String validatedUserId = request.getParameter("userid");
String validatedStartDate = request.getParameter("startdate");
validatedUserId =ESAPI.encoder().encodeForSQL(ORACLE_CODEC,validatedUserId)
validatedUserId= ESAPI.encoder().encodeForSQL(ORACLE_CODEC,validatedStartDate)
public String encodeCharacter( char[] immune, Character c ) {
//全部引号由单引号添为双引号
if ( c.charValue() == '\’’ )
return "\'\’”;
return ""+c;
}
3.3命令注入
介绍:由于业务需求,程序有可能要执行系统命令的功能,但如果执行的命令用户可控,业务上有没有做好限制,就可能出现命令执行漏洞。
审计策略:搜索命令执行的函数Runtime.exec,Process,ProcessBuilder.start,GroovyShell.evaluate,看这些命令是否可控。
漏洞示例:(未对用户输入进行过滤,)
String btype = request.getParameter("btype");
String cmds[] = {"cmd.exe",
"/K",
”dir”
+directory};
System.Runtime.getRuntime().exec(cmds);
代码规范示例:
String btype = ce.CmdFliter(request.getParameter(”ip"));
Public String CmdFliter (String cmd){
Properties prop = System.getProperties();
String os = prop.getProperty("os.name");
String[] vulnchars = {"|",";","&","$",">","<","`","\\","!"};
String[] escapeUnixchars = {"\\|","\\;","\\&","\\$","\\>","\\<","\\`","\\\\","\\!"};
String[] escapeWinchars = {"`|","`;","`&","`$","`>","`<","``","`\\","`!"};
String[] replacechar = null;
String cmds = cmd.tochar();
Boolean needFilter=true;//用以标记
while(needFilter){
for(int i =0;i<cmds.length;i++)
// 遍历替换,如没有可替换的字符,则设置needFilter为false,不再循环
}
return cmds.toString();
}
3.4 xml实体注入
介绍:由于解析过程中没有限制doctype、entity等节点实体的解析,导致的XML外部实体解析漏洞。
审计策略:全局搜索如下字符串StreamSource, XMLConstants, StringReader在项目中搜索. Xsd文件
漏洞示例:
<!DOCTYPE z [<!ENTITY test SYSTEM " file:///etc/passwd/ " >]>
<users>
<a>&test;</a>
<username>Alice</username>
<email>Alice@cmbc</email>
</users>
代码规范示例:(通过配置解析方式,杜绝非法实体,DOM解析是优先选择,StAX解析会完全禁止DTD)
DOM 解析:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
String FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
dbf.setFeature(FEATURE, true);
SAX 解析:
SAXParserFactory saxpf = SAXParserFactory.newInstance();
String FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
saxpf.setFeature(FEATURE, true);
StAX 解析:
XMLInputFactory factory = XMLInputFactory.newInstance();
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
3.5文件下载:
介绍:对用户的访问权限不做限制,未过滤../导致用户可以回溯到任意目录。下载敏感文件,产生危害。
审计策略:
查找 getPath, getAbsolutePath。
再排查程序的安全策略配置文件,搜索 permission Java.io.FilePermission 字样和 grant 字样,防止误报。换句话说,如果 IO 方案中已经做出防御。只为程序的绝对路径赋予读写权限,其他目录不赋予读写权限。那么目录系统还是安全的。
漏洞示例:
String imgName = request.getParameter("imgName");
String imgKey = MD5Encrypt.MD5(imgName);//本地
if (imageCache.containsKey(imgKey)) {
data = (byte[]) imageCache.get(imgKey);
} else {
String imagePath = Consts.IMG_LOCAL_PATH + imgName; //直接拼接了本地址和文件名
InputStream inputStream = null;
File imageFile = new File(imagePath);
if (imageFile.exists() && imageFile.isFile()) {
inputStream = new FileInputStream(imagePath);
int i = inputStream.available();
data = new byte[i];
imageCache.put(imgKey, data);
}
}
OutputStream outputStream = response.getOutputStream();
outputStream.write(data);
代码规范示例:(使用id映射在后端获取文件地址,文件名)
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
try {
ImageDao imgDao=new ImageDao();
byte data[] = new byte[1];
String imgID = request.getParameter("imgID");
String imgName=imgDao.getImage(imgID);
String imgKey = MD5Encrypt.MD5(imgName);//本地
if (imageCache.containsKey(imgKey)){
data = (byte[]) imageCache.get(imgKey);
}
else{String imagePath = Consts.IMG_LOCAL_PAHT + imgName;}
// 根据文件路径读取文件输出到 data 里
}catch{ // error todo
}
// 将文件内容输出到客户端
}
代码规范示例:(全局过滤../(linux) ..\\(win))
if(request.getParameter("file")!=null){
String context = request.getContextPath();
Boolean flag = false;
int BUFSIZE = 4096;
String filePathOrig = request.getParameter("file");
File file = new File(request.getRealPath(context));
String filePath = file.getParent()+"/upload/"+filePathOrig;
filePath = filePath.replace("..\\",""); //win
filePath = filePath.replace(“../”,“”); //linux、unix
if(filePath != filePathOrig){
return;
}
file = new File(filePath);
// 文件读取并返回客户端
}
代码规范示例(判断用户权限,判断文件类型,是否能够输出)
if(request.getParameter("file")!=null){
String context = request.getContextPath();
int BUFSIZE = 4096;
String filePath;
filePath = request.getParameter("file");
File file = new File(request.getRealPath(context));
file = new File(file.getParent()+"/upload/"+filePath);
//判断请求下载文件是否处于指定的目录
if(file.getCanonicalPath().startsWith("/var/webapp/upload")==-1){
return;
}
//文件读取并返回客户端
}
3.6文件上传
介绍:对上传的文件未做校验,导致攻击者上传精心构造的木马,产生危害。
审计策略:
1:白名单或者黑名单校验后缀(白名单优先)
2:上传的文件是否校验限制了文件的大小(文件太大会造成dos)
3:是否校验文件上传的后缀。关键函数如下
IndexOf(“.”) 从前往后取第一个点 被绕过可能 1.jpg.jsp
修复方案:IndexOf()替换成lastIndexOf()
4:文件后缀对比
string.equals(fileSuffix)次函数不区分大小写。可通过string.Jsp这种方式绕过。修复方案在比较之前之前使用 fileSuffix.toLowerCase() 将前端取得的后缀名变换成小写或者改成s.equalsIgnoreCase(fileSuffix) 即忽略大小
5:是否通过文件类型来校验
String contentType = file.getContentType();
这种方式可以前端修改文件类型绕过上传
6、java程序中涉及到文件上传的函数,比如:
MultipartFile
7、模糊搜索相关文件上传类或者函数比如
File
FileUpload
FileUtils
UploadHandleServlet
FileLoadServlet
getInputStream
FileOutputStream
DiskFileItemFactory
MultipartRequestEntity
漏洞示例:
String contentType = request.getContentType();
String pLine = new String();
String uploadLocation = new String(UPLOAD_DIRECTORY_STRING);
if (contentType != null && contentType.equals(“image/jpg”) != -1) { //只判断了contentType
BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream()));
pLine = br.readLine();
try {
String filename = pLine.substring(pLine.lastIndexOf("\\"), pLine.lastIndexOf("\""))
BufferedWriter bw = new BufferedWriter(new FileWriter(uploadLocation+filename, true));
for (String line; (line=br.readLine())!=null; )
// 写文件操作
}
}
代码规范示例:(对文件类型进行校验)
public static boolean checkImage(byte[] imgData){
BufferedImage bufferedImage = null;
ByteArrayInputStream bais = new ByteArrayInputStream(imgData);
try {
// 生成 Image 子类对象,方便对图片判断
bufferedImage = ImageIO.read(bais);
// 判断能否获取有效像素
if(bufferedImage.getHeight()==-1||bufferedImage.getWidth()==-1){
return false;
}
return true;
} catch (IOException e) {
return false;
}
}
代码规范示例:(自定义补课预测文件名,将文件上传目录直接设置为不可执行,移除可执行权限,并二次渲染)
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取文件上传数据操作…………
// 将上传文件保存于非解析目录
String uploadPath ="/var/FileUpload/";
DateFormat df=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 自定义不可预测文件名
String filename = df.format(new Date())+String.Random(8)+".jpg";
File file = new File(uploadPath+filename);
boolean check = CheckFile.isValid(file);
if(!check){
return;
}
// 写入文件操作…………
// 移除文件的可执行权限并二次渲染
file.setExecutable(false);
}
3.7跨站请求伪造(csrf)
介绍:跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种使已登录用户在不知情的情况下执行某种动作的攻击。因为攻击者看不到伪造请求的响应结果,所以CSRF攻击主要用来执行动作,而非窃取用户数据。当受害者是一个普通用户时,CSRF可以实现在其不知情的情况下转移用户资金、发送邮件等操作;但是如果受害者是一个具有管理员权限的用户时CSRF则可能威胁到整个Web系统的安。
审计策略:
此类漏洞一般都会在框架中解决修复,所以在审计csrf漏洞时。首先要熟悉框架对CSRF的防护方案,一般审计时可查看增删改请求重是否有token、formtoken等关键字以及是否有对请求的Referer有进行校验。手动测试时,如果有token等关键则替换token值为自定义值并重放请求,如果没有则替换请求Referer头为自定义链接或置空。重放请求看是否可以成功返回数据从而判断是否存在CSRF漏洞。
漏洞示例:
由于开发人员对CSRF的了解不足,错把“经过认证的浏览器发起的请求”当成“经过认证的用户发起的请求”,当已认证的用户点击攻击者构造的恶意链接后就“被”执行了相应的操作。例如,一个博客删除文章是通过如下方式实现的:
GET http://blog.com/article/delete.jsp?id=102
当攻击者诱导用户点击下面的链接时,如果该用户登录博客网站的凭证尚未过期,那么他便在不知情的情况下删除了id为102的文章,简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
代码规范示例:(使用token解决请求固定问题)
@RequestMapping("updateData")
public boolean updateData(HttpServletRequest req,HttpServletResponse resp){
String referer=req.getHeader("Referer");
// 判断 Referer 是否以正确域名开头
if((referer!=null) &&(referer.trim().startsWith("https://www.test.com/"))){
HttpSession s = request.getSession();
String sToken = (String)s.getAttribute("csrftoken");
// 从请求参数中取得 CSRF Token
String pToken = req.getParameter("csrftoken");
if(sToken != null && pToken != null && sToken.equals(pToken)){
// 获取请求数据并更新
}else{ request.getRequestDispatcher("error.jsp").forward(request,response);
}
}
4.组件安全
4.1 fastjson
修复建议:
将fastjson框架统一更新到1.2.60及以上版本;同时需关闭autotype属性,将任意版本使用以下代码请删除:ParserConfig.getGlobalInstance().setAutoTypeSupport(true)
4.2 jackson-databind:
修复建议:
方式1:关闭Default Typing属性
方式2:升级版本至jackson-databind >= 2.9.9
4.3 shiro:
修复建议:
方式1:禁用rememberMe功能
方式2:升级版本至shrio version>1.2.4
同时(方式1、方式2均需要进行),为预防第三方开源框架整合shiro,请在源代码中搜索securityManager.setRememberMeManager(rememberMeManager);、setCipherKey(Base64.decode(" ,如果发现请删除securityManager.setRememberMeManager(rememberMeManager);或者修改Shrio内置秘钥key,确保key的唯一性。
4.4 struts2:
修复建议:
方式1:升级版本至struts2>=2.3.35或 struts2>=2.5.17(建议此方案)
方式2:根据自己当前系统版本,进行临时修复(可作为参考,建议进行升级)
同时,通过组件、框架及其相关漏洞的识别,构建我行软件资产库,实现软件资产识别及软件资产应急响应,主要包括组件识别、框架识别以及相应漏洞识别等。