【软件开发规范四】《应用系统安全编码规范》
时间:2022-09-20 07:30:00
应用系统安全编码规范
目??录
应用系统安全编码规范
目??录
应用系统安全编码规范
1 总则
1.1 目的
1.2 适用范围
1.3 阅读对象
2 术语和定义
2.1 应用系统
2.2 敏感信息
2.3 鉴别
2.4 验证码
3 缩略语
4 应用安全编码规范
4.1身份认证
4.2访问控制
4.3 输入输出验证
4.4 会话安全
4.5 数据安全
5 禁止项
5.1绝对禁止项
5.2安全问题
- 总则
- 目的
为了落实信息安全战略的要求,有效加强应用系统的安全管理,提高应用系统的安全编码能力,指导开发团队有效编码应用系统的安全编码,制定本规范。
- 适用范围
本指南适用于集团公司和子公司的所有应用系统。
- 阅读对象
适合本文档的阅读对象包括:
- 应用系统需求、设计师、开发项目项目经理;
- 开发和维护应用系统人员;
- 应用系统安全功能测试人员、安全漏洞测试人员等。
- 术语和定义
- 应用系统
由一个或多个应用程序(通常为定制开发)组成,并可能结合若干其它通用软件(如中间件、数据库等),部署在操作系统上,实现特定的功能需求。从架构上区分主要包括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 '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; &bsp;} 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 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 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中配置过滤器及权限:
|
- 避免使用不够随机的数值
(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(); } |
绝对路径下载:
- 防范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, username, hashedPassword from some_table where id = ${id} |
如果使用${}的格式,直接在 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, username, hashedPassword from some_table where id = #{id} |
输入合法性验证示例:
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("Title:" + rs.getString("title") + ""); out.print(" out.print(" } } |
绿色代码是通过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()) { %> | |||
<%=rs.getInt("id") %> | <%=rs.getString("pname")%> | <%=rs.getString("pdesc")%> | <%=rs.getString("ptype")%> |
代码中加粗的变量“rs.getInt("id")、rs.getString("pname")、rs.getString("pdesc")和rs.getString("ptype")”均为从数据库中读取到的数据,被直接输出到了页面中,没有做任何安全过滤,若从数据库中获取到的数据中包含JS/VBS脚本,就可能导致用户浏览器把JS/VBS脚本执行,从而造成XSS攻击。
(3)合规方案
对输出的数据进行编码输出,用来确保字符被视为数据,而不是作为代码被浏览器解析。
安全编码示例:
HTML实体/属性编码
在HTML/XML中显示“用户可控数据”前,应该进行htmlescape。
#escapeHTML($user.name) | #escapeHTML($user.name) |
所有HTML和XML中输出的数据,都应该做html escape转义。
escapeHTML需要进行html转义应该按照以下列表进行转义
& < > " ' |
& < > " ' |
JavaScript编码
html转义并不能保证在脚本执行区域内数据的安全,也不能保证脚本执行代码的正常运行。
|
针对输出在script标签之间的数据,需要转义的字符有:
/ ' " \ |
\/ \' \" \\ |
CSS编码
在style内容中输出的“用户可控数据”,需要做CSS escape转义。
String safe = ESAPI.encoder().encodeForCSS( request.getParameter("input")); |
AJAX输出编码
XML输出“用户可控数据”时,对数据部分做HTML转义。
|
JSON安全编码
JSON输出时要先对变量内容中的“用户可控数据”单独作htmlEscape,再对变量内容做一次JavasSript Escape。
String cityname=”北京”+StringUtil.htmlEscape(city.name)+””; 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来更新用户的信息。攻击者可在恶意站点中构造如下表单,诱使登陆者点击。
document.form1.submit(); |
(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(" out.print("--------------------- for (int i = 0; i < nodes.getLength(); i++) { out.print(nodes.item(i).getNodeName()+" : " + nodes.item(i).getFirstChild().getNodeValue().toString()); out.print(" } |
(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,最少采取以下措施: