# 中国电信软件研发规范安全分册(修订版) > 脱敏整理版:已移除编制人员、联系人和联系方式,并合并无意义硬换行。技术条款、章节和示例以原始 DOCX 为争议核验依据。 附件 11中国电信软件研发规范安 全 分 册 (修订版) 中国电信集团有限公司 ## 2023 年 12 月 附录 3 引用 193 > 编制人员信息已移除。 版本变更历史 ## 1 文档说明 1. 编制说明 本文档是为了进一步规范中国电信软件安全研发流程, 提升研发人员安全编码意识和安全威胁防范能力, 指导开发人员在保证系统安全性的情况下完成系统开发, 从而有助于在编码阶段减少安全漏洞的产生, 在部署阶段规避常见的安全问题, 提升整体软件或系统的安全防护能力。2. 文档结构 本文档主要分为三个章节:(1) 软件安全基本原则, 规定了软件研发过程应遵循的基本安全原则。(2) 软件安全开发规范, 规定了常用语言的安全编码规范。(3) 代码安全扫描要求, 规定了代码安全扫描的基本要求 、扫描工具 、评估方法以及应达到的代码安全基线, 以实现软件安全基本原则以及开发规范的落地实施。3. 适用范围 本规范适用于指导中国电信软件研发工作。 4. 起草单位 本规范的起草单位是中国电信集团公司。 5. 解释权 本规范解释权属于中国电信集团公司。 6. 版权 本规范的版权属于中国电信集团公司。 7. 名词解释 8. 2. 软件安全基本原则 ### 2.1 安全设计 #### 2.1.1 默认拒绝原则 (Fail-Safe Defaults) ● 只要没有授权的信息就不允许访问 ● 不能出现本该允许的请求被拒绝与本该拒绝的请求被允许 #### 2.1.2 开放设计原则 (Open Design) ● 不将安全机制的设计作为秘密, 不将系统安全性寄托在保守安全机制设计秘密的基础上● 应在隔开安全机制设计方案的前提下, 借助容易保护的特定元素, 如密钥、口令等来增强系统的安全性● 开放设计有助于安全机制接受广泛的审查 #### 2.1.3 特权分离原则 (Separation of Privilege) ● 细分特权, 分配给多个主体, 减少每个特权拥有者的权利● 禁止 root 用户远程访问 #### 2.1.4 最小特权原则 (Least Privilege) ● 每个程序和每个用户应该只拥有完成工作所需特权的最小集合 ● 限制由意外或错误所引起的破坏 ● 将特权程序之间的潜在交互数降低到正确操作所需的最小值, 尽量避免非经意 、不必要 、不恰当的使用特权 #### 2.1.5 最少公共机制原则 (Least Common Mechanism) ● 将多个用户公用或被全体用户依赖的机制数量降到最少 #### 2.1.6 完全仲裁原则 (Complete Mediation) ● 授权检查覆盖任何一个访问操作 ● 安全机制有能力标识每一个访问操作请求的所有源头 ● 能够对待为了提高性能而缓存的检查结果 #### 2.1.7 零信任原则(Zero Trust) ● 要严格限制用户 、外部部件的信任度, 要假设他们都是不安全的。 #### 2.1.8 会话管理原则 ● Session ID 必须足够随机且无法预测;● 若会话状态中含有敏感信息, 必须使用 SSL 加密客户端和服务端之间包含Session ID 值的任何通信;● 服务器端认证通过或权限变动时, 必须重新生成 Session ID;● 避免未经授权访问会话状态;● 禁止非一次有效身份凭证在 URL 中传输;● 避免未经校验的数据直接给会话赋值;● 在一些要求登录状态的场景下, 客户端和服务端应该保持一个会话(Session);● 建议尽可能缩短会话寿命。 ● 限制同一用户的会话数, 尽量同一用户只允许一个会话存在。 #### 2.1.9 缓存管理原则 ● 所有缓存数据, 必须设置过期时间;● 不能在要求强一致性的系统使用缓存, 若使用缓存, 必须确保最终一致性;● 为避免冲突, 缓存复用时应该先清除再赋值。● 系统设计时应该预防缓存雪崩 、缓存穿透;● 建议做好缓存的性能指标监控, 并及时调整缓存设置;常用的缓存性能指标包括: 缓存命中率 、响应时间 、TPS 、 内存使用量等。 #### 2.1.10 访问控制原则 ● 严禁使用用户输入参数作为访问控制的依据;● 所有访问失败场景 、管理操作都必须记录日志。 ● 必须限制默认角色的访问权限, 特别对于生产环境, 应该避免设置默认角色, 或仅给予最低授权;● 必须对访问的数据进行用户或角色控制;● 专用管理后台必须与应用服务相独立, 应用服务理论上无权限访问管理后台;● 同一种应用程序应该只存在一种基于角色的访问控制;● 应该在会话处于非活跃一定时间或会话结束后终止网络连接;● 应该在服务或进程之间启用访问控制;● 前端页面展示应该基于角色进行访问控制, 特权页面非授权用户不可见;对于重定向页面, 也需保持权限一致;● 系统访问建议采用基于角色的访问控制, 可通过设置用户 、角色 、系统访问矩阵, 决定允许或拒绝用户对受控系统进行资源访问; ● 建议无权限访问返回页面保持一致; ### 2.2 安全开发 #### 2.2.1 输入验证原则 ● 所有输入数据都可能产生安全问题, 需要经过安全性验证。 #### 2.2.2 客户端不可信任原则 ● 所有输入都必须在服务端进行验证, 确保输入数据安全可信。● 对于客户端请求, 标识用户身份等的敏感数据须从服务端获得, 不应直接使用客户端传输的身份信息。 #### 2.2.3 错误消息原则 ● 展示给用户的错误信息应使用包含编号的一般错误信息, 不应该暴露关于系统 、 网络或应用程序的敏感信息。 #### 2.2.4 URL 加密传输原则 ● URL 中包含的敏感信息应加密传输。 #### 2.2.5 注释代码处理原则 ● 注释代码中不能包含敏感信息。 #### 2.2.6 最小化原则 ● 输入信息最小化, 尽可能减少用户输入。● 返回信息最小化, 仅返回必要的数据, 程序错误信息对用户屏蔽。 #### 2.2.7 失败终止原则 ● 对不符合输入验证的数据, 应终止业务的执行, 不要试图修正和转换用户提交的参数继续向下执行。 ### 2.3 安全部署 #### 2.3.1 最小授权原则 ##### 2.3.1.1 用户管理 ● 慎用 sudo 提权, 禁止以 root 权限运行业务应用;● 不要以 root 用户在容器内运行(必要的 mount 操作除外)● 一般场景下, 应该创建服务专属帐号来启动服务; ##### 2.3.1.2 文件管理 ● Linux 下, 创建临时文件或目录, 应该使用 mktemp 命令;● Linux 下, 应该根据属主 、属组为文件设置合理的权限, 通常情况下, 避免设置文件权限为 777。 ##### 2.3.1.3 防火墙管理 防火墙默认禁止所有的域间流量, 所有未明确允许的流量都被禁止 。这是在防火墙上落实最小授权原则的基础 。在此基础上, 仅为合法流量开放安全策略, 可以有效减小攻击面。第一, 为合法流量开放安全策略时, 请谨慎使用 Any 作为匹配条件, 建议设置尽可能精确的匹配条件 。所谓精确的匹配条件, 包括两个方面:● 限制到具体的源/目的 IP 地址 、服务● 设置尽可能多的匹配条件, 如用户 、应用等第二, 请为临时安全策略设置生效时间段。第三, 请关注安全策略的方向。 #### 2.3.2 记录日志原则 日志记录了网络中的业务运行状态 、流量分布 、应用分布等信息, 是获得网络可视性的基础 。记录日志有助于问题定位 、 回溯取证和策略优化。 #### 2.3.3 定期审计原则 网络变化 、业务变化都可能造成安全风险被隐藏, 定期审计是解决这个难题的一个有效方法。 ### 2.4 禁止弱口令 弱口令认定: 对于采取静态口令认证技术的系统或设备, 帐户口令应满足以下要求:1. 口令长度应至少 8 位;2. 口令应包括数字 、小写字母 、大写字母 、特殊符号 4 类中的 3 类;3. 口令应与用户名无相关性, 口令中不得包含用户名的完整字符串 、大小写变位或形似变换的字符串;4.应更换系统或设备的默认出厂口令;5. 口令设置应避免键盘排序密码;不满足以上任意一项的口令相关问题, 可定义为“弱口令 ”漏洞 。对于通过其他技术手段或社会工程方式(如社会库碰撞 、密码表爆破 、配置文件解密 、社会工程学 、监听探测或利用其他安全漏洞等), 获取到帐户口令的相关问题,不定义为“弱口令 ”。3. 软件安全开发规范 ### 3.1 通用安全开发规范 3.1.1 【强制】禁止在日志中保存口令 、密钥和其他敏感数据 规则描述: 在日志中不能输出口令 、密钥和其他敏感信息, 口令包括明文口令和密文口令 。对于敏感信息建议采取以下方法:(1) 不在日志中打印敏感信息。(2) 若因为特殊原因必须要打印日志, 则用固定长度的星号(*) 代替输出的敏感信息。 3.1.2 【强制】禁止使用私有或者弱加密算法 规则描述: 禁止使用私有算法或者弱加密算法(比如 DES, SHA1 、MD5 等)。应该使用经过验证的 、安全的 、公开的加密算法。加密算法分为对称加密算法和非对称加密算法。推荐使用的对称加密算法有: SM4推荐使用的非对称加密算法有: SM2推荐使用的数字签名算法有: SM2推荐使用的摘要算法: SM3如果使用哈希算法来存储口令, 则必须加入盐值(salt)对每个推荐的算法, 其密钥长度需符合以下最低安全要求: 1) SM4: 128 位 2) SM2: 256 位 3.1.3 【强制】基于哈希算法的口令安全存储必须加入盐值 实践中, 一个口令可以编码为一个哈希值, 且无法从哈希值逆向计算出原始的口令 。 口令是否相等可以通过比较它们的哈希值是否相等来判断 。如果一个口令的哈希值储存在一个数据库中, 由于哈希算法的不可逆性, 攻击者就应该不可能还原出口令 。如果说可以恢复口令, 那么唯一的方式就是暴力破解攻击, 比如计算所有可能口令的哈希值, 或是字典攻击, 计算出所有常用的口令的哈希值 。如果每个口令都只仅经过简单哈希, 相同的口令将得到相同的哈值。仅保存口令哈希有以下两个缺陷: 1) 由于“生日判定 ”, 攻击者可以快速找到一个口令, 尤其是当数据库中的口令数量较大的时候。 2) 攻击者可以使用事先计算好的哈希列表在几秒钟之内破解口令 。为了解决这些问题, 可以在进行哈希运算之前在口令中引入盐值 。一个盐值是一个固定长度的随机数 。这个盐值对于每个存储入口来说必须是不同的 。可以明文方式紧邻哈希后的口令一起保存 。在这样的配置下, 攻击者必须对每一个口令分别进行暴力破解攻击 。这样数据库便能抵御“生日 ”或者“彩虹表 ”攻击。 3) 为了减慢哈希的计算速度, 推荐进行 n 次迭代操作 。虽然对一个口令进行n 次哈希对于攻击者和典型用户来说的确减慢了哈希, 但是典型用户并不会有太大的感知, 因为哈希的时间相对于他们与系统互动的总时间来说只是非常小的一个比例 。另一方面, 攻击者破解时几乎 100%的时间花在哈希计算上, 所以哈希 n 次以 n 为因子减慢了攻击者的速度, 而对于典型用户几乎不可察觉 。建议: a) 盐值至少应该包含 8 字节而且必须是由安全随机数产生。 b) 应使用强哈希函数, 推荐使用 SM4 或者更加安全的哈希函数。 推荐默认进行 50000 次哈希, 至少对有性能限制(比如说嵌套系统) 的产品进行 5000 次以上哈希。 3.1.4 【强制】禁止将敏感信息硬编码在程序中 如果将敏感信息(包括口令和加密密钥) 硬编码在程序中, 可能会将敏感信息暴露给攻击者 。任何能够访问到 class 文件的人都可以反编译 class 文件并发现这些敏感信息 。 因此不能将信息硬编码在程序中 。 同时, 硬编码敏感信息会增加代码管理和维护的难度。 3.1.5 【强制】应该使用强随机数 伪随机数生成器(PRNG) 使用确定性数学算法来产生具有良好统计属性的数字序列。但是这种数字序列并不具有真正的随机特性 。伪随机数生成器通常以一个算术种子值为起始 。算法使用该种子值生成一个输出以及一个新的种子, 这个种子又被用来生成下一个随机值, 以此类推。Java API 提供了伪随机数生成器(PRNG)—— java.util.Random 类 。这个伪随机数生成器具有可移植性和可重复性 。 因此, 如果两个 java.util.Random类的实例创建时使用的是相同的种子值, 那么对于所有的 Java 实现, 它们将生成相同的数字序列 。在系统重启或应用程序初始化时, Seed 值总是被重复使用 。在一些其他情况下, seed 值来自系统时钟的当前时间 。攻击者可以在系统的一些安全脆弱点上监听, 并构建相应的查询表预测将要使用的 seed 值。因此, java.util.Random 类不能用于安全敏感应用或者敏感数据保护 。应使用更加安全的随机数生成器, 例如 java.security.SecureRandom 类。正确示例:public byte[] genRandBytes( int len ) {byte[] bytes = null;if ( len > 0 && len < 1024 ) {bytes = new byte[len];SecureRandom random = new SecureRandom();random.nextBytes( bytes ); }return(bytes); 3.1.6 【强制】禁止在生产环境部署工程或主干及商用发布分支中保留任何后门程序 后门程序说明:1.包括为便于开发者和测试工程师访问一部分终端用户不能访问的程序代码而预留的指令。2.代码中监听了除正常业务通信范围以外的端口。3.主动向未经许可的远端通信服务器发送任何数据请求。 ### 3.2 Java 安全开发规范 #### 3.2.1 数据校验 3.2.1.1【强制】禁止对用户输入的未经过滤的参数进行 SQL 拼接 说明:使用未经过滤的参数进行 SQL 拼接, 会导致 SQL 注入漏洞的产生 。而SQL 注入漏洞会造成数据泄露, 网页被篡改等严重危害 。推荐使用参数化查询 方式执行 SQL 语句 。如进行 SQL 拼接, 必须对输入的参数进行危险字符校验。错误示例(Java 代码动态构建 SQL):Statement stmt = null;ResultSet rs = null;try: { String userName = ctx.getAuthenticatedUserName(); //this is a constant String sqlString = "SELECT * FROM t_item WHERE owner='" + userName + "' AND itemName='" + request.getParameter("itemName") + "'";stmt = connection.createStatement();rs = stmt.executeQuery(sqlString); // ... result set handling } catch (SQLException se) { // ... logging and error handling } 这里将查询字符串常量与用户输入进行拼接来动态构建 SQL 查询命令。仅当 itemName 不包 含单引号时, 这条查询语句的行为才会是正确的 。如果一个攻击者以用户名 wiley 发起一个 请求, 并使用以下条目名称参数进行查询: name' OR 'a' = 'a那么这个查询将变成:SELECT * FROM t_item WHERE owner = 'wiley' AND itemname = 'name' OR 'a' ='a';此处, 额外的 OR 'a'='a'条件导致整个 WHERE 子句的值总为真 。那么,这个查询便等价于如下非常简单的查询:SELECT * FROM t_item这个简化的查询使得攻击者能够绕过原有的条件限制: 这个查询会返回 items 表中所有储存 的条目, 而不管它们的所有者是谁, 而原本应该只返回属于当前已认证用户的条目。正确示例(使用 PreparedStatement 进行参数化查询):PreparedStatement stmt = null ResultSet rs = null try {String userName = ctx.getAuthenticatedUserName(); //this is a constant String itemName = request.getParameter("itemName"); // ...Ensure that the length of userName and itemName is legitimate // ... String sqlString = "SELECT * FROM t_item WHERE owner=? AND itemName=?"; stmt = connection.prepareStatement(sqlString); stmt.setString(1, userName);stmt.setString(2, itemName);rs = stmt.executeQuery(); // ... result set handling }catch (SQLException se) { // ... logging and error handling } 如果使用参数化查询, 则在 SQL 语句中使用占位符表示需在运行时确定的参数值 。参数化查询使得 SQL 查询的语义逻辑被预先定义, 而实际的查询参数值则等到程序运行时再确定 。参数化查询使得数据库能够区分 SQL 语句中语义逻辑和数据参数, 以确保用户输入无法改变预期的 SQL 查询语义逻辑。在 Java 中, 可以使用 java.sql.PreparedStatement 来对数据库发起参数化查询 。在这个正确示例中, 如果一个攻击者将 itemName 输入为 name' OR 'a'= 'a, 这个参数化查询将免受攻击, 而是会查找一个 itemName 匹配 name' OR 'a' = 'a 这个字符串的条目。3.2.1.2【强制】禁止对用户输入的未经过滤的参数进行 XML 拼接 说明:和 SQL 注入原理相同, XML 是存储数据的一种格式 。如果在查询或修改时, 没有做转义而直接拼接, 将导致 XML 注入漏洞的产生 。攻击者可以修改XML 数据格式, 增加新的 XML 节点, 对数据处理流程产生不可预估的影响。错误示例:(未检查的用户输入)private void createXMLStream(BufferedOutputStream outStream, User user) throws IOException { String xmlString;xmlString = "operator" + user.getUserId() + "" + user.getDescription() + "";outStream.write(xmlString.getBytes());outStream.flush(); }某个恶意用户可能会使用下面的字符串作为用户 ID: "joeadministratorjoe"并使用如下正常的描述作为描述字段: "I want to be an administrator"最终, 整个 XML 字符串将变成如下形式:operatorjoeadministrator joeI want to be an administrator 由于 SAX 解析器(org.xml.sax and javax.xml.parsers.SAXParser)在解释XML 文档时会将第二个 role 域的值覆盖前一个 role 域的值, 因此导致此用户角色由操作员提升为了管理员。正确示例: 白名单校验private void createXMLStream( BufferedOutputStream outStream, User user ) throws IOException { /* Write XML string if userID contains alphanumeric and underscore characters only */if ( !Pattern.matches( "[_a-bA-B0-9]+", user.getUserId() ) ) {/* Handle format violation */ } if ( !Pattern.matches( "[_a-bA-B0-9]+", user.getDescription() ) ) { /* Handle format violation */ } String xmlString = "" + user.getUserId()+ "operator"+ user.getDescription() + "";outStream.write( xmlString.getBytes() ); outStream.flush(); }这个方法使用白名单的方式对输入进行清理, 要求输入的 userId 字段中只能包含字母 、数 字或者下划线。正确示例(使用安全的 xml 库):public static void buidlXML(FileWriter writer, User user) throws IOException {Document userDoc = DocumentHelper.createDocument();Element userElem = userDoc.addElement("user");Element idElem = userElem.addElement("id");idElem.setText(user.getUserId());Element roleElem = userElem.addElement("role");roleElem.setText("operator");Element descrElem = userElem.addElement("description");descrElem.setText(user.getDescription());XMLWriter output = null; try { OutputFormat format = OutputFormat.createPrettyPrint();format.setEncoding("UTF-8");output = new XMLWriter(writer, format); output.write(userDoc);output.flush(); } finally {try {output.close(); }catch (Exception e) { // handle exception } } } 这个正确示例使用 dom4j 来构建 XML, dom4j 是一个良好定义的 、开源的 XML 工具库 。Dom4j 将会对文本数据域进行 XML 编码, 从而使得 XML的原始结构和格式免受破坏 。 这个例子中, 如果攻击者输入如下字符串作为用户 ID: "joeAdministratorI want to be an administrator" 则最终会生成如下格式的 XML:operatorjoe</id><role>Administrator</role><!— operator--><description>I want to be an administrator 可以看到, “< ”与“> ”经过 XML 编码被分别替换成了“< ”与“> ”,导致攻击者未能将其角色类型从操作员提升到管理员。 3.2.1.3【强制】禁止用户输入未经校验的系统命令作为入参 说明:有时, 由于业务需要, 系统命令需要接受用户输入, 如果对此输入不做任何限制, 恶意攻击者可以利用这个功能控制服务器, 对服务器进行恶意破坏。反例: 未加任何限制, 允许任意命令输入Runtime.getRuntime().exec(request.getParameter("cmd"));正例:所有需要执行的系统命令, 必须做好白名单限制, 如果不在白名单范围内的, 直接拒绝来自客户端请求。String cmd = request.getParameter("cmd").toLowerCase();//此处假定传入的参数不为空String[] whiteList = new String[]{"ipconfig","netstat"}; for(String str:whiteList){if(str.contains(cmd)){ return false; } } return true; 3.2.1.4【强制】禁止向前端页面输出未经安全过滤或未正确转义的数据 说明:如果向前端输入的数据不做任何过滤或转义, 恶意用户可以利用该缺陷往动态页面中注入恶意脚本, 造成跨站脚本编制攻击漏洞出现 。通过该漏洞, 恶意用户可以获取用户的账号密码, 改变用户设置, 窃取 cookies 亦或向页面插入一些恶意内容。反例: 未加任何限制, 允许任意命令输入<%=request.getParameter("username")%>正例: 对特殊字符进行转义操作ESAPI.encoder().encodeForHTML(username) 上述是利用 ESAPI 接口进行转义: 3.2.1.5【强制】禁止上传未经安全校验的文件(文件路径操作) 说明:系统在处理用户上传的文件时, 没有判断文件的扩展名是否在允许的范围内, 就把文件保存在服务器上, 导致恶意用户可以上传任意文件, 甚至上传脚本木马到服务器, 进而控制服务器 。又或者未校验上传的文件大小, 导致过大文件上传服务器会占用磁盘空间。反例: 未做任何验证操作@ResponseBody@RequestMapping(value="importFile")public String importFile(MultipartFile uploadFile){ File file = new File("D:"+ File.separator+"temp"); try {uploadFile.transferTo(file); } catch (IOException e) { e.printStackTrace(); } } 正例: 做白名单验证及大小验证@ResponseBody@RequestMapping(value="importFile")public String importFile1(MultipartFile uploadFile){ String fileName = uploadFile.getOriginalFilename();String fileType = fileName.substring(fileName.lastIndexOf(".") + 1);if("txt".equals(fileType.toLowerCase()) && uploadFile.getSize()<10000000){ File file = new File("D:"+ File.separator+"temp");try {uploadFile.transferTo(file); } catch (IOException e) { e.printStackTrace(); } } } 3.2.1.6【强制】页面跳转过程中使用的 URL 参数应该进行白名单验证 说明:应用系统在接收用户提交的 URL 参数后, 如果没有及时对参数进行可信任验证, 任由其输入任意 URL 进行跳转, 则会带来网站钓鱼的风险, 造成账号被盗等恶意事件出现。反例: 未做任何验证操作response.sendRedirect(request.getParameter("url"));正例: 做白名单验证String url = request.getParameter("url");if(url.startsWith("http://test.com")) { response.sendRedirect(); } 前端传来一个 url, 需要校验 3.2.1.7【强制】 禁止向 Runtime.exec() 方法传递不可信 、未净化的数据 (命令行注入) 在执行任意系统命令或者外部程序时使用了未经校验的不可信输入, 就会导致产生命令和参数注入漏洞 。对于命令注入漏洞, 命令将会以与 Java 应用 程序相同的特权级别执行, 它向攻击者提供了类似系统 shell 的功能 。在 Java中, Runtime.exec()经常被用来调用一个新的进程, 但是这个调用并不会通过命令行 Shell 来执行 。 因此, 无法通过链接或者管道的方式来连续执行多个命令。但是, 如果在 Runtime.exec()中使用命令行 shell(例如, cmd.exe 或 /bin/sh) 来调用一个程序, 则可被命令注入 。 当通过 Runtime.exec()来运行 bat 或者 sh 脚 本时, 命令行 shell 将自动被调用 。 例如, Runtime.getRuntime().exec("test.bat & notepad.exe"), 由于 bat 文件默认是由命令行解释器 cmd.exe 来解释执行的,这里的 “& ”符号将会被 cmd.exe 当做一个命令分隔符, 从而导致 test.bat 与notepad.exe 都将 会被执行 。 当参数中包含空格, 双引号, 以-或者/符号开头表示一个参数开关时, 可能会导 致参数注入漏洞。错误示例:class DirList {public static void main(String[] args) { if (args.length == 0) {System.out.println("No arguments"); System.exit(1); } try {Runtime rt = Runtime.getRuntime();Process proc = rt.exec("cmd.exe /c dir " + args[0]); // ... } catch (Exception e) { // Handle errors } } } 攻击者可以通过以下命令来利用这个漏洞程序: java DirList "dummy & echo bad"实际将会执行两个命令:dir dummy echo bad 这会试图列举一个不存的文件夹 dummy 中的文件, 然后往控制台输出bad。 #### 3.2.2 异常行为 3.2.2.1【强制】禁止异常抛出敏感数据信息 说明:敏感数据的范围应该基于应用场景以及产品威胁分析的结果来确定 。典型的敏感数据包括口令 、银行账号 、个人信息 、通讯记录 、密钥等 。如果在传递异常的时候未对其中的敏感信息进行过滤常常会导致信息泄露, 而这可能帮助攻击者尝试发起进一步的攻击 。攻击者可以通过构造恶意的输入参数来发掘应用的内部结构和机制 。不管是异常中的文本消息, 还是异常本身的类型都可能泄露敏感信息 。 例如, 对于 FileNotFoundException 异常, 其中的异常消息会透露文件系统的结构信息, 而通过异常本身的类型, 可以得知所请求的文件不存在 。 因此, 当异常会被传递到信任边界以外时, 必须同时对敏感的异常消息和敏感的异常类型进行过滤。实现方式:异常本身的处理错误示例(异常消息和类型泄露敏感信息):public class ExceptionExample {public static void main(String[] args) throws FileNotFoundException { // Linux stores a user's home directory path in // the environment variable $HOME, Windows in %APPDATA% // ... Other omitted code FileInputStream fis = new FileInputStream(System.getenv("APPDATA") + args[0]); } } 此错误示例代码中, 当请求的文件不存在时, FileInputStream 的构造器会抛出 FileNotFoundException 异常 。这使得攻击者可以不断传入伪造的路径名称来重现出底层文件系统结构。错误示例 (封装后重新抛出敏感异常) :try {FileInputStream fis = new FileInputStream( System.getenv( "APPDATA" ) + args[0] ); } catch ( FileNotFoundException e ) {/* Log the exception */throw new IOException( "Unable to retrieve file", e ); }此错误示例中, 即使用户无法访问到日志中记录的异常, 但是原始的异常仍然会被广播, 攻击者可能会利用这点来发现敏感的文件系统结构信息。错误示例 (异常净化)class SecurityIOException extends IOException {/* ... */ }; // ... try {FileInputStream fis = new FileInputStream(System.getenv("APPDATA") + args[0]); } catch (FileNotFoundException e) { // Log the exception throw new SecurityIOException(); }与前面几个错误示例相比, 此例代码抛出的异常虽然泄露的有用信息较少,但是它仍然会透露出指定的文件不可读 。程序对于存在的文件路径输入和不存在的文件路径输入会有不同的反应 。 因此, 攻击者可以根据程序的行为推断出文件系统的敏感信息 。未对用户输入做限制, 使得系统面临暴力攻击的风险,攻击者可以多次传入所有可能的文件名进行查询来发现有效文件: 如果传入一个文件名后, 程序返回一个净化后的异常, 则暗示该文件不存在, 而如果不抛出异常则说明该文件是存在的。正确(安全策略):public class ExceptionExample {public static void main(String[] args) { File file = null;try {file = new File(System.getenv("APPDATA") + args[0]).getCanonicalFile();if (!file.getPath().startsWith("c:\\homepath")) { System.out.println("Invalid file");return; } } catch (IOException x) { System.out.println("Invalid file");return; }try { FileInputStream fis = new FileInputStream(file); } catch (FileNotFoundException x) { System.out.println("Invalid file"); return; } } } 在这个正确示例中, 规定用户只能打开 c:\homepath 目录下的文件, 用户不可能发现这个目录以外的任何信息 。在这个方案中, 如果无法打开文件, 或者文件不在合法的目录下, 则会产生一条简洁的错误消息 。任何 c:\homepath目录以外的文件信息都被会隐蔽起来。正确示例(安全输入):public class ExceptionExample {public static void main(String[] args) { FileInputStream fis = null;try {switch (Integer.valueOf(args[0])) {case 1:fis = new FileInputStream("c:\\homepath\\file1");break;case 2:fis = new FileInputStream("c:\\homepath\\file2");break; // ... default:System.out.println("Invalid option");break; } } catch (Throwable t) { MyExceptionReporter.report(t); // Sanitize any sensitive data } } } 这个正确示例限制用户只能打开 c:\homepath\file1 与 c:\homepath\file2 。 同时, 它也会过滤在 catch 块中捕获的异常中的敏感信息。 3.2.2.2【强制】不允许忽略被捕获的异常 说明:对于已经捕获异常的代码要进行处理, 不能忽略已经捕获的异常, 导致出现不良的代码以及后续问题的定位 、修复。实现方式:错误:public class ExceptionExample {public static void example() { try {FileInputStream fis = new FileInputStream(System.getenv("jdbc ")); } catch (FileNotFoundException e) { //不做处理 } } } 正确:public class ExceptionExample {public static void example() {try{FileInputStream fis = new FileInputStream( System.getenv( "jdbc " ) ); }catch ( FileNotFoundException e ) {log.error( “ 读取文件 错 误 { 0 } ”, e.getMessage() )return; } } 3.2.2.3【强制】不允许抛出 RuntimeException, Exception,Throwable 说明: RuntimeException 由于 java 设计中所有方法都默认定义在 throws 中,如果你不处理会导致异常一层层往上抛, 需要在最终出口处进行捕获处理。Exception 是其他所有异常的父类, 在声明方法扔出 Exception 的情况下, 无法对问题进行精准分析处理 。Throwable 是 Error 和 Exception 的父类, try catch只能捕获 Exception类型的异常, 一旦发生 error 问题无法做出后续处理程序可能崩溃, 而 Exception 可以做后续的比如数据回滚。实现方式:错误:public class ExceptionExample {public static void example(){ try{FileInputStream fis = new FileInputStream(System.getenv("jdbc ")); } catch(FileNotFoundException x){thow new Exception(“程序出错了 ”) } }不明异常 正确:public class ExceptionExample {public static void example(){ try{FileInputStream fis = new FileInputStream(System.getenv("jdbc ")); } catch(FileNotFoundException x){log.error(“读取文件错误{0} ”,x.getMessage())thow new CustomException(“程序出错了 ”) } }精准异常 #### 3.2.3 序列化和反序列化 3.2.3.1【强制】不要序列化未加密的敏感数据 说明:虽然序列化可以将对象的状态保存为一个字节序列, 之后通过反序列化该字节序列又能重新构造出原来的对象, 但是它并没有提供一种机制来保证序列化数据的安全性 。可访问序列化数据的攻击者可以借此获取敏感信息并确定对象的实现细节 。攻击者也可恶意修改其中的数据, 试图在其被反序列化之后对系统造成危害 。 因此, 敏感数据序列化之后是潜在对外暴露着的 。永远不应该被序列化的敏感信息包括: 密钥 、数字证书 、 以及那些在序列化时引用敏感数据的类 。此条规则的意义在于防止敏感数据被无意识的序列化导致敏感信息泄露。实现方式:通过使用 java 中的 transient 关键字, 让某些被修饰的成员属性变量不被序列化。错误示例:public class GPSLocation implements Serializable { private double x; // sensitive field private double y; // sensitive field private String id; // non-sensitive field // other content } public class Coordinates {public static void main(String[] args) { FileOutputStream fout = null;try {GPSLocation p = new GPSLocation(5, 2, "northeast");fout = new FileOutputStream("location.ser");ObjectOutputStream oout = new ObjectOutputStream(fout);oout.writeObject(p);oout.close(); } catch (Throwable t) { // Forward to handler } finally { if (fout != null) { try {fout.close(); } catch (IOException x) { // handle error } } } } } 在这段示例代码中, 假定坐标信息是敏感的, 那么将其序列化到数据流中使之面临敏感信息泄露与被恶意篡改的风险。正确示例( transient ):public class GPSLocation implements Serializable {private transient double x; // transient field will not be serialized private transient double y; // transient field will not be serialized private String id; // other content } 在将某个包含敏感数据的类序列化时, 程序必须确保敏感数据不被序列化。这包括阻止包含敏感信息的数据成员被序列化, 以及不可序列化或者敏感对象的引用被序列化 。该示例将相关字段声明为 transient, 从而使它们不包括在依照默认的序列化机制应该被序列化的字段列表中 。这样既避免了错误的序列化,又防止了敏感数据被意外序列化。 3.2.3.2【强制】避免序列化过程中出现内存和资源泄漏 说明:数据对象在序列化后使用不正确的情况下可能会导致所在的机器内存的泄漏, 某个对象不在使用, 由于回收的时候被其他实例调用, 导致 GC 无法回收,资源无法释放, 导致内存泄漏。实现方式:正例:class TelnetData{public static TelnetData readSensorData() {...} public static boolean isAvailable() {...} } class TelnetSerData {public static void main(String[] args) throws IOException { ObjectOutputStream out = null;try {out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("ser.dat")));while (TelnetData.isAvailable()) {TelnetData sd = TelnetData.readSensorData();out.writeObject(sd);out.reset(); // } } finally {if (out != null) { out.close(); } } } } 反例: class TelnetData implements Serializable {public static TelnetData readSensorData() {...} public static boolean isAvailable() {...} } class TelnetSerData {public static void main(String[] args) throws IOException { ObjectOutputStream out = null;try {out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("ser.dat")));while (TelnetData.isAvailable()) {TelnetData sd = TelnetData.readSensorData();out.writeObject(sd); } } finally { if (out != null) { out.close(); } } } } 3.2.3.3【强制】反序列化避免恶意代码 说明:由于 Apache Commons Collections 允许链式的任意的类函数反射调用 ,Java 类 ObjectInputStream 在执行反序列化时本身不会对输入进行检查, 恶意攻击者也可能通过构建特定的输入, 在序列化完成后会产生非正常结果, 利用这一方法可以实现远程执行任意代码, 主要使用 Runtime.exec(String cmd)函数 来执行外部命令 。建议升级 Apache Commons Collections 版本至 3.2.2 以上版本,降低危害性, 提供 JVM 安全性。实现方式:应重载 ObjectInputStream 类的 resolveClass 方法, 校验(3.2.2 不用重载此方法, 可参看官方提供文档)正例public final class SecureObjectInputStream extends ObjectInputStream{ public SecureObjectInputStream() throws IOException{super(); }public SecureObjectInputStream(InputStream in) throws IOException{ super(in); } protected Class resolveClass(ObjectStreamClass desc) throws ClassNotFoundException, IOException{if (!desc.getName().equals("java_security.Person")) {throw new ClassNotFoundException(desc.getName()+" not found"); }return super.resolveClass(desc); } } 反例:class ExportObject {public static String exec(String cmd) throws Exception { String sb = "";BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());BufferedReader inBr = new BufferedReader(new InputStreamReader(in));String lineStr;while ((lineStr = inBr.readLine()) != null) sb += lineStr + "\n";inBr.close();in.close(); return sb; }public ExportObject() throws Exception {String cmd="open /Applications/cmd.app/"; throw new Exception(exec(cmd)); } } #### 3.2.4 I/O 操作 3.2.4.1【强制】临时文件需要及时删除 说明:程序员经常会在全局可写的目录中创建临时文件 。 例如, POSIX 系统下的/tmp 与/var/tmp 目录, Windows 系统下的 C:\TEMP 目录 。这类目录中的文件可能会被定期清理, 例如, 每天晚上或者重启时 。然而, 如果文件未被安全地创建或者用完后还是可访问的, 具备本地文件系统访问权限的攻击者便可以利用共享目录中的文件操作 。删除已经不再需要的临时文件有助于对文件名和其他资源(如二级存储) 进行回收利用 。每一个程序在正常运行过程中都有责任确保删除已使用完毕的临时文件。错误示例:public class TempFile {public static void main(String[] args) throws IOException { File f = new File("tempnam.tmp");if (f.exists()) {System.out.println("This file already exists");return; }FileOutputStream fop = null;try { fop = new FileOutputStream(f);String str = "Data";fop.write(str.getBytes()); } finally {if (fop != null) { try {fop.close(); } catch (IOException x) { // handle error } } } } } 这个错误示例代码在运行结束时未将临时文件删除。正确示例( JDK1.7: DELETE_ON_CLOSE )public class TempFile {public static void main( String[] args ) {Path tempFile = null;try {tempFile = Files.createTempFile( "tempnam", ".tmp" );try (BufferedWriter writer = Files.newBufferedWriter( tempFile,Charset.forName( "UTF8" ), StandardOpenOption.DELETE_ON_CLOSE ) ) {/* write to the file and use it */ } System.out.println( "Temporary file write done, file erased" ); } catch ( IOException x ) {/* Some other sort of failure, such as permissions. */System.err.println( "Error creating temporary file" ); } } } 这个正确示例创建临时文件时用到了 JDK1.7 的 NIO2 包中的几个方法。它使用了 createTempFile()方法, 这个方法会新建一个随机的文件名(文件名的构造方式由具体的实现所定义, JDK 缺少相关的文档说明)。文件使用 try-with-resources 构造块来打开, 这种方式将会自动关闭文件, 而不管是否有异常发生, 并且在打开文件时用到了 DELETE_ON_CLOSE 选项, 使得文件在关闭时会被自动删除。正确示例(手动删除临时文件):public class TempFile {public static void main(String[] args) throws IOException { File f = File.createTempFile("tempnam", ".tmp");FileOutputStream fop = null;try {fop = new FileOutputStream(f); // write to the file and use it } finally { if (fop != null) { try {fop.close(); } catch (IOException x) { // handle error } if (!f.delete()) // delete file when finished { // log the error } } } } } 对于 JDK1.7 之前的版本, 可以在临时文件使用完毕之后 、系统终止之前,显式地对其进行删除。3.2.4.2【强制】不要将 Buffer 对象封装的数据暴露给不可信代码。 说明Java.nio 包中的类 Buffer , 如 IntBuffer, CharBuffer 以及 ByteBuffer 定义了一些方法, 如 warp, slice, duplicate, slice , 所有这些方法虽然会返回一个新的buffer 对象, 但是在底层仍然使用原来的数组, 任何对 buffer 对象的修改都会导致底层的数据被修改 。外部不可信的代码可能会通过这种恶意修改来影响系统中同样使用底层数组的逻辑。错误示例:public class Wrapper {private char[] dataArray;public Wrapper() {dataArray = new char[10]; // Initialize } public CharBuffer getBufferCopy() { return CharBuffer.wrap(dataArray); } } public class Duplicator { CharBuffer cb; public Duplicator() {cb = CharBuffer.allocate(10); // Initialize } public CharBuffer getBufferCopy() { return cb.duplicate(); } } 这两个错误示例代码声明了一个 char 数组, 然后将此数组封装到一个buffer 中, 最后通过 getBufferCopy()方法将此 buffer 暴露给不可信代码。Java 语言相关正确样例如下:public class Wrapper {private char[] dataArray; public Wrapper() { // Initialize dataArray = new char[10]; } // return a read-only view public CharBuffer getBufferCopy() {return CharBuffer.wrap(dataArray).asReadOnlyBuffer(); } } public class Duplicator {CharBuffer cb;public Duplicator() { // Initialize cb = CharBuffer.allocate(10); } // return a read-only view public CharBuffer getBufferCopy() { return cb.asReadOnlyBuffer(); } } 这个正确示例以只读 CharBuffer 的方式返回 char 数组的一个只读视图。 3.2.4.3【建议】建议在多用户系统中创建文件时指定合适的访问许可 说明在多用户操作系统中, 比如 LINUX, 一个文件通常属于特定的用户 。文件的拥有者可以将文件上面的特定权限(比如只读) 授予给其它人员, 这样避免将无关的权限(可写) 授予其它人员后产生相关的安全漏洞。Java 语言相关正确样例如下:Path file = new File("file").toPath(); // 只对文件授予新建与附加的权限 Set options = new HashSet();options.add(StandardOpenOption.CREATE_NEW);options.add(StandardOpenOption.APPEND); // 用户只能拥有读取与写入的权限Set perms = PosixFilePermissions.fromString("rw-------"); FileAttribute> attr =PosixFilePermissions.asFileAttribute(perms);try (SeekableByteChannel sbc = Files.newByteChannel(file, options, attr)) { // write data }; 3.2.4.4【强制】避免让外部进程阻塞在输入输出流上 说明 java.lang.Runtime 类的 exec()方法与相关联的 ProcessBuilder.start() 方法可以用来调用外部程序, 这些外部程序由 java.lang.Process 类进行描述, 其包括一个输入流, 一个输出流, 一个错误流 。如果输入流没有输入则会导致阻塞 。如果输出流或者错误流没有被及时读取, 则会耗尽相关的缓冲区, 这样的结果是 java 程序会被阻塞。Java 语言相关正确样例如下:public class Exec {public static void main(String[] args)throws IOException, InterruptedException { Runtime rt = Runtime.getRuntime();Process proc = rt.exec("notemaker"); // 使用新线程的方式读取异常流和输入流 StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), System.err) // 读取输入流 StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), System.out);errorGobbler.start();outputGobbler.start();int exitVal = proc.waitFor(); // Any error errorGobbler.join(); // 等待读取异常流的线程结束outputGobbler.join(); // 等待读取输入流的线程结束 } } // 下面是读取流的线程类 class StreamGobbler extends Thread { InputStream is;PrintStream os;StreamGobbler(InputStream is, PrintStream os) { this.is = is;this.os = os; }public void run() { try {int c;while ((c = is.read()) != -1)os.print((char) c); } catch (IOException x) { // handle error } } } 3.2.4.5【强制】避免在共享目录操作文件 说明多用户系统允许多个具有不同权限的用户共享一个文件系统 。攻击者可以利用许多文件系统的特性和功能, 包括文件链接 、设备文件和共享文件访问,对文件进行越权访问, 特别是在多个用户可以创建 、移动 、删除文件的共享目录中操作文件时 。 当程序以较高权限运行时, 可能被攻击者利用来提升自己的权限 。为了防止漏洞, 程序必须仅在安全目录中操作文件 。如果对于某个特定用户, 只有该用户与系统管理员可以在其中创建 、移动 、删除文件, 那么这个目录就是安全的。错误示例Stringfile= request.getParameter( "webDAVPath" );InputStreamin= null; try { in = new FileInputStream( file ); /* ... */ } finally {try {if ( in != null ) {in.close(); } } catch ( IOException x ) {/* handle error */ } } 在这个错误示例中, 攻击者可以指定一个锁定设备或者一个先入先出(FIFO) 文件名, 导致程序在打开文件时挂起。正确示例:由于共享目录固有的可访问性以及潜在的条件竞争, 文件必须在安全目录中进行操作 。 由于程序可能以较低的权限运行, 并缺少构建安全目录的能力,如果一个给定的路径不是在安全目录中, 则程序需要抛出异常。下面的解决方式是针对 POSIX 系统的一个 isInSecureDir()方法实现 。这个方法确保传入的文件及其所有上层目录是归当前用户或者管理员所有, 任何其他的用户不能对其进行写操作, 并且其所有上层目录也不能被任何其他用户删除或者改名(除了系统管理员)。public static boolean isInSecureDir(Path file) {return isInSecureDir(file, null); } public static boolean isInSecureDir(Path file, UserPrincipal user) { return isInSecureDir(file, user, 5); }/** * 检测文件对于当前用户是否位于一个安全目录中 * @param file 待测文件 * @param user 相关的用户, 如果其为 null, 则默认是当前用户 * @param symlinkDepth 允许的符号链接文件的深度 * @return true 如果文件自在的目录是安全目录则返回 true,反之返回 false */ public static boolean isInSecureDir(Path file, UserPrincipal user, int symlinkDepth) { if (!file.isAbsolute()) {file = file.toAbsolutePath(); }if (symlinkDepth <= 0) { // Too many levels of symbolic links return false; } // 获得用户与超级用户的用户信息 FileSystem fileSystem = Paths.get(file.getRoot().toString()).getFileSystem();UserPrincipalLookupService upls = fileSystem.getUserPrincipalLookupService(); UserPrincipal root = null;try {root = upls.lookupPrincipalByName("root");if (user == null) {user = upls.lookupPrincipalByName(System.getProperty( "user.name")); } if ((root == null) || (user == null)) { return false; } } catch (IOException x) { return false; } // 目录的所有父目录中如果不安全, 此目录也不安全// dir is not secure for (int i = 1; i <= file.getNameCount(); i++) {Path partialPath = Paths.get(file.getRoot().toString(), file.subpath(0, i).toString());try {if (Files.isSymbolicLink(partialPath)) {if (!isInSecureDir(Files.readSymbolicLink(partialPath),user, symlinkDepth - 1)) { // 是文件链接, 所在的目录不是安全目录return false; } } else { UserPrincipal owner = Files.getOwner(partialPath);if (!user.equals(owner) && !root.equals(owner)) { // 目录由其它用户拥有, 不安全return false; }PosixFileAttributes attr = Files.readAttributes(partialPath,PosixFileAttributes.class);Set perms = attr.permissions();if (perms.contains(PosixFilePermission.GROUP_WRITE) ||perms.contains(PosixFilePermission.OTHERS_WRITE)) { // someone else can write files, not secure return false; } } catch (IOException x) { return false; } } return true; } } 调用方法如下:String fileName =" provided by user ";Path path = new File(fileName).toPath(); try {if (!isInSecureDir(path)) {System.out.println("File not in secure directory"); return; } // 其它代码 … } catch (IOException x) { // handle error } 3.2.4.6【建议】建议用字符数组存储密码 说明因为字符串是不可变对象, 如果作为普通文本存储密码, 那么它会一直存在内存中直至被垃圾收集器回收 。 因为字符串从字符串池中取出的(如果池中有该字符串就直接从池中获取, 否则 new 一个出来, 然后把它放入池中), 这样有很大的机会长期保留在内存中, 这样会引发安全问题 。 因为任何可以访问内存的人能以明码的方式把密码 dump 出来 。另外你还应该始终以加密而不是 普通的文本来表示密码 。 因为字符串是不可变, 因此没有任何方法可以改变其内容, 任何改变都将产生一个新的字符串, 而如果使用 char[], 你就可以设置所有的元素为空或者为零(这里作者的意思是说, 让认证完后该数组不再使用了, 就可以用零或者 null 覆盖原来的密码, 防止别人从内存中 dump 出来)。所以存储密码用字符数组可以明显的减轻密码被盗的危险。Java 语言相关样例如下:String strPassword="Unknown"; //不建议的写法char[] charPassword= new char[]{'U','n','k','w','o','n'}; //建议的写法 #### 3.2.5 接口安全 3.2.5.1【建议】建议对接口中重要的参数进行加密处理 说明:如果接口中的参数进行明文传输, 有可能会泄露一些敏感信息, 如用户登录模块采用明文密码传输, 则会造成密码泄露等。实现方式:对重要接口参数进行加密处理, 建议使用符合业内安全强度标准的加密算法, 如 RSA 算法, AES256 算法等。3.2.5.2【建议】建议对重要的接口加入令牌机制 说明:在某些情况下, 用户会在无保护意识的情况下携带登录正规网站的 cookie去访问恶意的网站, 该恶意网站会让用户访问该正规网站中的某些功能, 从而导致一些数据的更改, 删除等, 造成 CSRF 漏洞。实现方式:在请求的接口中加入随机 token 。 当客户端请求页面时, 服务端生成随机数 token, 并且将 token 存放在服务端, 如 session 中, 然后将 token 发给客户端 。下次客户端提交请求时, 在请求中附带 token 发送给服务端 。此时服务端将自己保存的 token 与客户端发来的 token 进行比较, 如果一致, 则请求有效, 否则拒绝请求。 3.2.5.3【建议】建议对接口中返回的敏感报文进行加密处理 说明:有些系统中某个模块会根据接口的返回报文进行逻辑判断处理 。如果此时返回的报文未做任何加密处理, 攻击者可以猜测报文含义, 进而利用工具拦截报文, 修改返回的结果, 可造成越权, 数据篡改等问题的出现。实现方式:对返回报文进行加密处理, 建议使用符合业内安全强度标准的加密算法,如 RSA 算法, AES256 算法等。 #### 3.2.6 多线程编码安全 3.2.6.1【强制】需要确保共享变量的可见性 对于共享变量, 要确保一个线程对它的改动对其他线程是可见的 。 线程可能会看到一个陈旧的共享变量的值 。为了共享变量是最新的, 可以将变量声明为 volatile 或同步读取和写入操作 。 将共享变量声明为 volatile:final class ControlledStop implements Runnable {private volatile boolean done = false;public void run() { while (!done) { try { // ... Thread.currentThread().sleep(1000); // Do something } catch(InterruptedException ie) { Thread.currentThread().interrupt(); // Reset interrupted status } } } public void shutdown() {done = true; } } 同步读取和写入操作:final class ControlledStop implements Runnable {private boolean done = false;public void run() {while (!isDone()) { try { // ... Thread.currentThread().sleep(1000); // Do something } catch(InterruptedException ie) { Thread.currentThread().interrupt(); // Reset interrupted status } } } public synchronized boolean isDone() {return done; } public synchronized void shutdown() {done = true; } } 3.2.6.2【强制】需要确保共享变量的操作是原子的 除了要确保共享变量的更新对其他线程可见的, 还需要确保对共享变量的操作是原子的, 这时将共享变量声明为 volatile 往往是不够的 。需要使用同步机制或 Lock 同步读取和写入操作:final class Flag {private volatile boolean flag = true; public synchronized void toggle() { flag ^= true; // Same as flag = !flag; }public boolean getFlag() { return flag; } } //使用读取锁确保读取和写入操作的原子性final class Flag { private boolean flag = true;private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock readLock = lock.readLock();private final Lock writeLock = lock.writeLock();public void toggle() { writeLock.lock(); try {flag ^= true; // Same as flag = !flag; } finally { writeLock.unlock(); } } public boolean getFlag() { readLock.lock();try {return flag; } finally { readLock.unlock(); } } } 3.2.6.3【强制】需要确保执行阻塞操作的线程可以终止 通常, 我们通过“ 中断 ”方式终止处于“ 阻塞状态 ”的线程 。 当线程由于被调用了 sleep(), wait(), join()等方法而进入阻塞状态; 若此时调用线程的interrupt()将线程的中断标记设为 true 。 由于处于阻塞状态, 中断标记会被清除,同时产生一个 InterruptedException 异常 。将 InterruptedException 放在适当的 位置就能终止线程, 形式如下:public void run() { try {while (true) { // 执行任务... } } catch (InterruptedException ie) { // 由于产生 InterruptedException 异常, 退出 while(true)循环, 线程 终止! } } 在 while(true)中不断的执行任务, 当线程处于阻塞状态时, 调用线程的interrupt()产生 InterruptedException 中断 。 中断的捕获在 while(true)之外, 这样就退出了 while(true)循环! 值得注意的是: 对 InterruptedException 的捕获务一般放在 while(true)循环体的外面, 这样, 在产生异常时就退出了 while(true)循环 。否则, InterruptedException 在 while(true)循环体之内, 就需要额外的添加退出处理 。形式如下:public void run() { while (true) {try { // 执行任务... } catch (InterruptedException ie) { // InterruptedException 在 while(true)循环体内。 运行! 需要手动退出break; } } } 上面的 InterruptedException 异常的捕获在 whle(true)之内 。 当产生InterruptedException 异常时, 被 catch 处理之外, 仍然在 while(true)循环体内;要退出 while(true)循环体, 需要额外的执行退出 while(true)的操作。3.2.6.4【建议】不要在一个有限的线程池执行相互依存的任务 有限线程池指定可以同时执行在线程池中的线程数量的上限 。程序不得使用有限线程池线程执行相互依赖的任务 。可能会导致线程饥饿死锁, 所有的线程池执行的任务正在等待一个可用的线程中执行一个内部队列阻塞。 ### 3.3 WEB 安全开发规范 #### 3.3.1 输入输出验证 3.3.1.1【强制】预防跨站请求伪造 (CSRF) 安全威胁 Cross-Site Request Forgery(CSRF), 跨站请求伪造攻击。攻击者在用户浏览网页时, 利用页面元素(例如 img 的 src), 强迫用户的浏览器向 Web 应用程序发送一个改变用户信息的请求。由于发生 CSRF 攻击后, 攻击者是强迫用户向服务器发送请求, 所以会造成用户信息被迫修改, 更严重者引发蠕虫攻击。CSRF 攻击可以从站外和站内发起 。从站内发起 CSRF 攻击, 需要利用网站本身的业务, 比如“ 自定义头像 ”功能, 攻击者指定自己的头像 URL 是一个修改用户信息的链接, 当其他已登录用户浏览攻击者头像时, 会自动向这个链接发送修改信息请求。从站外发送请求, 则需要攻击者在自己的服务器上, 放一个自动提交修改个人信息的 html 页面, 并把页面地址发给用户, 用户打开时, 会发起一个请求。如果攻击者能够知道网站管理后台某项功能的 URL, 就可以直接攻击管理员, 强迫管理员执行攻击者定义的操作。 图 3.3.6 CSRF 危害 图 3.3.7 CSRF 攻击分析从图 3.3.7 可以看出, 要完成一次 CSRF 攻击, 用户必须依次完成两个步骤:1.登录受信任网站 A, 并在本地生成 Cookie。2.在不登出 A 的情况下, 访问危险网站 B。◆ 不能保证用户登录了一个网站后, 不再打开一个 tab 页面并访问另外的网站。◆ 不能保证用户关闭浏览器了后, 本地的 Cookie 立刻过期, 上次的会话已经结束 。(事实上, 关闭浏览器不能结束一个会话, 但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了…)上图中所谓的攻击网站, 可能是一个存在其他漏洞的可信任的经常被人访问的网站。解决方案 要防御 CSRF 攻击, 有以下几种方法: 1) 检查请求头中的 Referer 字段。 当该字段非空, 并且其值不是来自其他站点时, 访客允许请求, 否则拒绝。 2) 使用一次性令牌, 一种简单有效的防御方法 。每一个表单生成一个不可预测的随机数, 作为令牌(token), 并将随机数保存在 Session 中, 接收到表单后立即验证随机数。 3) 3) 使用 token 验证 。必须遵循以下三步: . 在用户登陆时, 设置一个 CSRF 的随机 TOKEN, 同时种植在用户的cookie 中, 当用户浏览器关闭 、 或用户再次登录 、 或退出时, 清除token。. 在表单中, 生成一个隐藏域, 它的值就是 COOKIE 中随机 TOKEN。 . 表单被提交后, 就可以在接收用户请求的 Web 应用中, 判断表单中的TOKEN 值是否和用户 COOKIE 中的 TOKEN 值一致, 如果不一致或没有这个值, 就判断为 CSRF 攻击, 同时记录攻击日志。由于攻击者无法预测每一个用户登录时生成的那个随机 TOKEN 值, 所以无法伪造这个参数。示例: 代码中$csrfToken.hiddenField 将会生成一个隐藏域, 用于生成验证 token,它将会作为表单的其中一个参数一起提交。建议使用 CSRF 防御框架, 覆盖所有 Web 应用。 注意:当出现 GET 请求修改用户数据时, 一旦在 url 中出现了 csrftoken, 当前页面就不允许出现用户定义的站外链接, 否则攻击者可以引诱用户点击攻击者定义的链接, 访问在自己的网站, 从 referer 中, 获取 url 中的 csrftoken, 造成 csrftoken 泄露。 4) 采用 Post 请求直接获取数据 。直接获取 form 表单数据不接收 URL 的,如 request.getForm("user"); 3.3.1.2【强制】预防代码注入 安全威胁 Code injection, 代码注入攻击。Web 应用代码中, 允许接收用户输入一段代码, 之后在 Web 应用服务器上执行这段代码, 并返回给用户。 由于用户可以自定义输入一段代码, 在服务器上执行, 所以攻击者可以写一个远程控制木马, 直接获取服务器控制权限, 所有服务器上的资源都会被攻击者获取和修改, 甚至可以直接控制数据库。解决方案 1) 白名单: 最好的防御代码注入漏洞的方式就是阻止包含可执行代码的用户输入 。任何用于动态代码的用户输入, 都必须进行无害化处理, 例如, 确保用户输入只包含白名单里的有效字符 。最好是在数据输入后, 通过使用用于存储和处理数据的提取方法, 立即执行数据无害化处理 。如果用户名中必须包含某些特殊字符, 那么必须先对它们进行标准化, 然后再对它们进行表单输入验证处理 。下面的示例使用了白名单来防止脚本引擎对未经处理的输入进行解析。 2) 安全沙盒: 另一种方式是使用安全管理器创建一个安全的沙盒 。应用程序应该阻止执行任意命令的脚本, 如查询本地文件系统 。双参数版本的 doPrivileged()方法可用于处理更低特权的操作, 这种情况下, 应用程序本身必须执行更高特权的操作, 但脚本引擎决不能拥有这样的特权 。对于默认策略文件中那些新创建的保 护域, RestrictedAccessControlContext 类减少了授予它们的权限 。这些有效权限是新创建的保护域和系统安全策略二者权限的交集。下面的示例演示了如何在双参数版本的 doPrivileged()中使用AccessControlContext。 将这个方法和白名单结合在一起使用, 可以获得更高的安全性。 3.3.1.3【强制】预防 XPath 注入 安全威胁 可扩展标记语言(Extensible Markup Language, XML) 可用于以类似于关系数据库的方式来存储数据 。XML 文档中的数据通常使用 XPath 来检索 。 当给XPath 检索例程提供的数据没有做合适的无害化处理时, 就有可能会导致XPath 注入(XPath injection)攻击。这种攻击类似于 SQL 注入或 XML 注入 。攻击者可以在查询用的数据字段中输入有效的 SQL 构造或 XML 构造 。典型的攻击是, 让条件查询字段解析为一个永真式, 这样就会导致攻击者访问到未经授权的信息。解决方案 为了防止 XPath 注入, 可以采用类似于防止 SQL 注入的方式:将所有的用户输入视为不可信, 并执行适当的无害化处理。对用户输入进行无害化处理时, 验证数据类型 、数据长度 、数据格式和数据内容的正确性。 例如, 使用一个正则表达式检查用户输入中是否包含 XML标签和特殊字符 。这种做法符合对用户输入进行无害化处理的规范。一种有效防止 SQL 注入相关问题的技术是参数化 。参数化能确保将用户 指定的数据以参数的形式传递给 API, 这样数据就不会被解释为可执行内容了。遗憾的是, Java SE 目前缺乏一个类似于 XPath 查询的接口 。不过, 通过使用 XQuery 这样的接口, XPath 可以模拟 SQL 参数化 。XQuery 支持将查询语句 写入运行时环境中的一个单独文件中。输入文件: login.xq 下面从一个文本文件中读取所需的特定格式的查询语句, 然后将用户名和密码的值插入一个映射中 。XQuery 库构造了这些来自用户输入的 XML 查询。 使用这种方法, 用户名(userName) 和密码(password) 字段中输入的数据不会被运行时环境解释为可执行的内容。防止 XPath 注入, 需要被删除(即禁止) 或适当转义的字符如下: ``< > / ' = "``可用于防止直接的参数注入。 XPath 查询不应该包含任何元字符(如' 、= 、* 、?、//或类似字符) 。 3.3.1.4【强制】预防系统命令注入 安全威胁 System command injection, 系统命令注入攻击。系统命令执行攻击, 是指代码中有一段执行系统命令的代码, 但是系统命令需要接收用户输入, 恶意攻击者可以通过这个功能直接控制服务器。 解决方案 所有需要执行的系统命令, 必须是开发人员定义好的, 不允许接收用户传来的参数, 加入到系统命令中去。3.3.1.5【强制】从 ZipInputStream 安全解压文件 安全威胁 对 java.util.ZipInputStream 的输入进行检查可以防止消耗过多的系统资源 。解压一个文件, 比如 zip、gif 或者 gzip 编码的 HTTP 内容, 可能会消耗过多的资源, 并且在压缩率极高的情况下, 可能会导致 zip 炸弹的出现 。 比较知名的 zip 炸弹是 42.zip, 它的大小只有 42KB, 但是解压之后居然有 4.5PB 之多。因为每个 zip 文件都包含了 16 个 zip 文件, 循环 5 次, 产生 16 的 5 次方个文件, 每个文件大小为 4.3GB。解决方案 在这个规范的代码示例中,代码在提取条目之前验证每个条目的名称 。如果名称无效, 那么整个提取就会被中止 。while 循环中的代码将判断 zip 归档中每个条目的文件大小, 同时提取条目 。如果提取的条目太大,在本例中为 100 MB, 则会抛出异常 。代码不要使用 ZipEntry.getSize()方法, 因为攻击者可以伪造 ZIP 文档中未压缩的文件的大小 。最后, 代码还计算压缩包中文件条目的数量, 如果超过 1024 个条目, 则抛出异常。除此之外, 还可以判断压缩文件中是否还有压缩文件, 尽量减少压缩套压缩的做法。3.3.1.6【强制】不要信任隐藏字段的内容 安全威胁 HTML 允许 Web 表单中的字段可见或隐藏 。 隐藏字段向 Web 服务器提供值, 但不能被用户修改其内容 。但是, 攻击者仍然可以通过特殊方式来修改隐蔽字段。解决方案 上面代码片段对隐藏字段进行净化, 因此, 当恶意 URL 进入浏览器时, servlet 产生以下内容: 3.3.1.7【强制】避免 URL 重定向到不信任的网站 安全威胁 如果使用未经验证的输入数据重定向 URL, 攻击者可能会成功的发动钓鱼攻击并窃取用户凭证。解决方案 3.3.1.8【强制】净化传递给正则表达式的非受信数据 安全威胁 非受信的输入应该在使用前净化, 从而防止发生正则表达式注入 。 当用户指定正则表达式作为输入时, 应注意需要保证初始的正则表达式没有被无限制 修改 。在用户输入字符串提交给正则解析之前, 进行白名单字符处理(比如字母和数字) 是一个很好的输入净化策略 。开发人员应仅仅提供最有限的正则表达式功能给用户, 从而减少被误用的可能。解决方案 这个符合规则的方案过滤搜索字符串中的非字母数字的字符(除了空格和单引号外), 这种方案可以阻止正则表达式注入。 3.3.1.9 【强制】预防 XSS 攻击 安全威胁 Cross Site Script(XSS), 跨站脚本攻击。攻击者利用应用程序的动态展示数据功能, 在 html 页面里嵌入恶意代码。当用户浏览该页之时, 这些嵌入在 html 中的恶意代码会被执行, 用户浏览器被攻击者控制, 从而达到攻击者的特殊目的。 跨站脚本攻击有三种攻击形式 1) 反射型跨站脚本攻击 攻击者会通过社会工程学手段, 发送一个 URL 连接给用户打开, 在用户打开页面的同时, 浏览器会执行页面中嵌入的恶意脚本。 2) 存储型跨站脚本攻击 攻击者利用 Web 应用程序提供的录入或修改数据功能, 将数据存储到服务器或用户 cookie 中, 当其他用户浏览展示该数据的页面时, 浏览器会执行页面中嵌入的恶意脚本 。所有浏览者都会受到攻击。 3) DOM 跨站攻击 由于 html 页面中, 定义了一段 JS, 根据用户的输入, 显示一段 html 代码, 攻击者可以在输入时, 插入一段恶意脚本, 最终展示时, 会执行恶意脚本。DOM 跨站和以上两个跨站攻击的差别是, DOM 跨站是纯页面脚本的输出,只有规范使用 JavaScript, 才可以防御。 恶意攻击者可以利用跨站脚本攻击做到: 1) 盗取用户 cookie, 伪造用户身份登录。 2) 控制用户浏览器。 3) 结合浏览器及其插件漏洞, 下载病毒木马到浏览者的计算机上执行。 4) 衍生 URL 跳转漏洞。 5) 让官方网站出现钓鱼页面。 6) 蠕虫攻击。 解决方案 为了避免 XSS 攻击, 建议采用以下方式进行防御: 1) 全局拦截(全局过滤器 、拦截器), 适用于不包含富文本的情况 Servlet 的 doFilter 、Spring 的 Interceptor 类, 对所有的访问请求进行监听。正确的姿势是在过滤器中对<>& ’”=等字符转义处理, 可使用 ESAPI 或者common-lang.jar 的 StringEscapeUtils 类或者 Spring 的 HtmlUtils 来实现。 2) 富文本交互, 白名单过滤 3) 设置 HttpOnly 属性, 避免攻击者利用跨站脚本漏洞进行 Cookie 劫持攻击。在 JavaEE 中, 给 Cookie 添加 HttpOnly 的代码如下: Spring MVC 预防 XSS 攻击的方法: 1) 项目级过滤 2) 页面级过滤 在包含 form 的 jsp 页面中添加 3) 表单元素级过滤 在 form 元素中添加 或 #### 3.3.2 数据加密与保护 3.3.2.1【强制】避免使用不安全的操作模式 安全威胁 块密码又称为分组加密, 一次加密明文中的一个块 。将明文按一定的位长分组, 明文组经过加密运算得到密文组, 密文组经过解密运算(加密运算的逆运算), 还原成明文组 。这种加密算法共有四种操作模式用于描述如何重复地应用密码的单块操作来安全的转换大于块的数据量, 分别是电子代码(ECB), 密码块链(CBC) 、密码反馈(CFB)以及输出反馈(OFB) 。其中 ECB 模式下相同的明文块总是会得到相同的密文, 故不能抵挡回放攻击, 而 CBC 模式则没有这个缺陷。解决方案 加密大于块的数据时, 需要注意避免使用 ECB 模式 。 由于 CBC 模式不会对相同的明文块生成相同的密文块, 所以 CBC 模式更好 。然而, CBC 模式效率较低, 并且在和 SSL 一起使用时会造成严重风险 。可以改用 CCM(Counter with CBC-MAC)模式, 如果更注重性能, 在可用的情况下则使用 GCM (Galois/Countcr)模式。以上代码将 AES 密码用于 CBC 模式。 3.3.2.2【强制】 限制敏感数据的生命周期 安全威胁 内存中的敏感信息很容易受到攻击, 导致泄漏 。对于以下条件, 不管应用程序满足哪一个, 能在应用程序所在的系统上执行代码的攻击者, 都能访问这些数据。 1) 使用对象来存储敏感数据, 但是内容在使用后没有被清除或没有被垃圾收集器回收。 2) 具有可以被操作系统按需(例如, 为了执行内存管理任务或者为了支持系统休眠) 交换到磁盘的内存页面。 3) 在缓冲区(如 BufferedReader) 中有敏感数据 。这些缓冲区持有敏感数据在操作系统缓存或内存中的副本。 4) 使用了一些反射机制来控制敏感数据的流动, 这些反射技巧可能规避掉系统对该数据的生命周期限制。 5) 通过调试信息 、 日志文件 、环境变量 、线程转储和核心转储等方式暴露敏感数据。 如果内存中包含的敏感数据在使用后没有及时被清除, 那么这些数据将极有可能被泄露 。为了降低敏感数据泄露的风险, 程序必须尽可能地最小化敏感数据的生命周期。完全缓解(即对内存数据万无一失的保护) 需要底层操作系统和 Java 虚拟机的支持 。 例如, 将敏感数据交换至磁盘, 但是这需要有一个禁用交换和休眠的安全操作系统。 解决方案 解决方案 1:使用 Console.readPassword()方法从控制台获取密码信息。 Console.readPassword()方法允许密码以字符序列的形式返回, 而不是以 String 对象的形式 。 因此, 程序员可以在使用密码后立即将其从数组中清除。同时这个方法也禁止将密码输出到控制台。 解决方案 2: 在下面的示例中, 程序使用了一个直接分配的新 I/O(new I/O, NIO) 缓冲区从文件中读取敏感数据 。这些数据可以在使用后被立即清除, 并且不会被缓存或缓冲在多个位置上, 它只会出现在系统内存中。注意, 必须手动清除缓冲数据, 因为垃圾收集器不会回收直接缓冲区。 #### 3.3.3 访问控制与日志安全 3.3.3.1【强制】SSL 连接时要进行服务器身份验证 安全威胁 当进行 SSL 连接时, 服务器身份验证处于禁用状态 。在某些使用 SSL 连接的库中, 默认情况下不验证服务器证书 。这相当于信任所有证书。解决方案 上述代码示例中,明确验证服务器证书。 3.3.3.2【强制】生成加密签名时保证必要加密步骤 安全威胁 在生成加密签名过程中, 代码缺少必要的步骤, 会削弱所生成签名的强度。 解决方案 上述代码片段中, 添加 update 的调用。 3.3.3.3【强制】避免使用 null 密码 安全威胁 null 密码会削弱系统的安全性, 甚至引起系统异常。 解决方案 上述代码中采用经过加密的密码值来获取数据库连接。 3.3.3.4【强制】避免使用空密码 安全威胁 程序中使用了空的密码值, 系统安全性将会受到威胁。 解决方案 上述代码中采用经过加密的密码值来获取数据库连接。 3.3.3.5【强制】避免使用明文密码 安全威胁 低系统安全性。 解决方案 应对程序中使用的密码值进行加密。 上述代码片段中,获取 connection 时使用已经加密的密码。 3.3.3.6【强制】避免越权访问 安全威胁 多用户系统中的文件通常为某一特定用户所有, 文件所有者会指定系统中的用户可以访问这些文件的内容 。 当一个程序使用不充分的限制性的访问权限创建文件时, 攻击者可以在程序修改权限之前读取或者修改这些文件 。如果允许用户输入直接更改文件权限, 则攻击者将能够访问其他受保护的系统资源。解决方案 上述代码可以通过允许用户设置文件权限来防止此类文件权限操纵 。如果必须确保用户拥有能够设置文件的权限, 则可以采用一种间接方式, 即创建一个允许用户进行指定的合法文件权限列表, 并且仅允许用户从列表中进行选择。通过这种方式, 用户提供的输入将不会直接更改文件权限。3.3.3.7【强制】禁止引用第三方脚本或指向第三方网站 安全威胁 Third-party script references, 引用第三方网站脚本或 iframe 指向第三方网站。第三方网站, 是指任何一个非本公司的网站。当 html 页面引用了第三方网站的脚本, 或者有 iframe 指向第三方网站时,一旦这个第三方网站出现安全问题, 被黑客控制写入恶意脚本, 那么该页面或脚本也会展示在网站上, 会导致网站的访问者间接受到影响。解决方案 禁止引用第三方脚本, 禁止 iframe 引用第三方页面。 3.3.3.8【强制】预防 URL 跳转攻击 安全威胁 Web 应用程序接收到用户提交的 URL 参数后, 没有对参数做“ 可信任URL ”的验证, 就向用户浏览器返回跳转到该 URL 的指令。如果 a.com 下的某个 Web 应用程序存在这个漏洞, 恶意攻击者可以发送给用户一个 a.com 的链接, 但是用户打开后, 却来到钓鱼网站页面, 将会导致用户被钓鱼攻击, 账号被盗, 或账号相关财产被盗。解决方案 为了保证用户所点击的 URL, 是从 Web 应用程序中生成的 URL, 所以要做 TOKEN 验证。 ## 1 、 当用户访问需要生成跳转 URL 的页面时, 首先生成随机 token, 并放入 cookie。 ## 2 、在显示连接的页面上生成 URL, 在 URL 参数中加入 token。 示例: ## 3 、 应用程序在跳转前, 判断 token 是否和 cookie 中的 token 一致, 如果不一致, 就判定为 URL 跳转攻击, 并记录日志。 ## 4 、 如果在 javascript 中做页面跳转, 需要判断域名白名单后, 才能跳转 。如果应用只有跳转到公司网站的需求, 可以设置白名单, 判断目的地址是 否在白名单列表中, 如果不在列表中, 就判定为 URL 跳转攻击, 并记录日志。不允许配置公司以外网站到白名单列表中。这两个方案都可以保证所有在应用中发出的重定向地址, 都是可信任的地址。 3.3.3.9【强制】预防垂直权限安全攻击 安全威胁 Vertical Access Control, 垂直权限安全攻击, 也就是权限提升攻击。由于 Web 应用程序没有做权限控制, 或仅仅在菜单上做了权限控制, 导致的攻击者只要猜测其他管理页面的 URL, 就可以访问或控制其他角色拥有的数据或页面, 达到权限提升目的。这个威胁可能导致普通用户变成管理员权限。 解决方案 在打开管理页面 URL 时, 首先判断当前用户是否拥有该页面的权限, 如果没有权限, 就判定为“权限提升 ”攻击, 同时记录安全日志。建议使用成熟的权限框架处理权限问题, 比如 spring security。 3.3.3.10【强制】预防水平权限安全攻击 安全威胁 Horizontal Access Control, 访问控制攻击, 也就是水平权限安全攻击。Web 应用程序接收到用户请求, 修改某条数据时, 没有判断数据的所属人,或判断数据所属人时, 从用户提交的 request 参数(用户可控数据) 中, 获取 了数据所属人 id, 导致恶意攻击者可以通过变换数据 ID, 或变换所属人 id,修改不属于自己的数据。攻击者可以删除或修改其他人数据。 解决方案 从用户的加密认证 cookie 中, 获取当前用户的 id, 并且需要在执行的 SQL 语句中, 加入当前用户 id 作为条件语句 。 由于是 Web 应用控制的加密算法, 所以攻击者无法修改加密信息。 代码中通过 GetUseridFromCookie, 从加密的 COOKIE 中获取了当前用户的id, 并加入到 SQL 语句中的 WHERE 条件中。3.3.3.11【强制】不要将未经验证的用户输入记录日志 安全威胁 当 日志条目包含未经净化的用户输入时会引发记录注入漏洞 。攻击者会插入伪造的日志数据, 从而让系统管理员以为是系统行为 。 例如, 用户在将冗长的日志记录拆分成两份时, 日志中使用的回车和换行符可能会被误解 。记录注入攻击可以通过对任何非受信的发送到日志的输入进行净化和验证来阻止。解决方案 这个符合规范的代码在登录之前会净化用户名输入, 从而防止注入攻击。 3.3.3.12【建议】Logging 安全日志记录 记录日志 在 Web 应用运行的过程中, 必须开启安全日志 。 当疑似攻击发生时, 对用户的当前请求, 记录日志 。在所有安全方案中需要记录日志的地方, 都应该按照本章节的要求记录日志, 以便回溯攻击场景。日志存储 日志文件要单独放在服务器上, 不能和 Web 容器的 log 放在同一个文件中, 并且是 http 无法直接访问到的地方, 例如日志存储要预留 http 接口, 以便需要时将日志通过 http, 发送到统一的服务器上。日志字段 字段按照以下要求记录, http request 包可配置, 平时可以不打开, 当受到攻击频繁时, 临时开启。 #### 3.3.4 资源使用安全 3.3.4.1【强制】需确保流得到释放 安全威胁 程序创建或分配流资源后, 不进行合理释放, 将会降低系统性能 。攻击者可能会通过耗尽资源池的方式发起拒绝服务攻击。解决方案 以上代码片段中, 在 finally 代码块中对流资源进行了合理的释放。 3.3.4.2【强制】需确保 Sockets 得到释放 安全威胁 程序创建或分配 Socket 后, 不进行合理释放, 将会降低系统性能 。攻击者可能会通过耗尽资源池的方式发起拒绝服务攻击。解决方案 上述代码片段中,使用完之前创建的 socket 套接字资源后, 在 finally 代码块中进行了释放。3.3.4.3【强制】避免拒绝服务攻击 安全威胁 拒绝服务是攻击者通过极度消耗应用资源, 以致程序崩溃或其他合法用户无法进行使用的一种攻击方式。解决方案 上述代码片段中,对文件大小进行检验, 并不设置读取长度。 3.3.4.4【强制】需确保数据库资源得到释放 安全威胁 程序创建或分配数据库资源后, 不进行合理释放, 将会降低系统性能 。攻击者可能会通过耗尽资源池的方式发起拒绝服务攻击。解决方案 上述代码片段中, 在 finally 代码块中合理释放了数据库资源。 3.3.4.5【强制】避免数据库访问控制 安全威胁 程序未进行恰当的访问控制, 执行的 SQL 语句包含用户所控制的主键, 可能会导致攻击者访问未经授权的记录。解决方案 上述代码片段中,通过把当前被授权的用户名作为查询语句的一部分来限制用户对清单的访问。3.3.4.6【强制】需确保文件得到释放 安全威胁 程序创建或分配文件句柄后, 不进行合理释放, 将会降低系统性能, 攻击者可能会通过耗尽资源池的方式发起拒绝服务攻击。解决方案 上述代码片段中, 使用完之前创建的文件句柄 zf 后, 在 finally 代码块中进行了释放。3.3.4.7【强制】在终止前移除临时文件 安全威胁 创建临时文件通常需要赋予其独特和不可预测的文件名, 不仅如此, 还需要在不需要它的时候删除它 。每一个程序会负责保证在正常操作的情况下删除临时文件, 但却没有万无一失的办法可以保证在异常情况下删除临时文件, 甚至采用 finally 块也不行 。基于这个原因, 很多系统使用临时文件删除工具来清除临时目录并删除旧文件 。违反该条准则可能会导致信息泄露和资源耗尽。解决方案 Java7 的 nio2 包 中 的 若 干 方 法 可 以 创 建 临 时 文 件 , 该 样 例 采 用Files.createTempFile()方法创建了一个不可预测名称的临时文件, 这个方法使用try 的构造函数打开, 不管文件是否出现异常, 它都会自动关闭文件 。最后, 使用 Java 7 的 DELETE_ON_CLOSE 选项打开文件, 这选项能让在关闭文件的时候文件被删除。3.3.4.8【强制】避免任意文件下载攻击和目录遍历攻击 安全威胁 File download and Directory traversal, 任意文件下载攻击和目录遍历攻击。 处理用户请求下载文件时, 允许用户提交任意文件路径, 并把服务器上对应的文件直接发送给用户, 这将造成任意文件下载威胁 。如果让用户提交文件目录地址, 就把目录下的文件列表发给用户, 会造成目录遍历安全威胁。攻击者可能会使用一些特殊的字符(如“ .. ”和“/ ”)摆脱受保护的限制, 或者通过变换目录或文件地址, 来下载服务器上的敏感文件 、数据库链接配置文件 、 网站源代码等。解决方案 对文件操作功能, 做到以下几点: 1) 要下载的文件地址保存至数据库中。 2) 文件路径保存至数据库, 让用户提交文件对应 ID 下载文件。 3) 下载文件之前做权限判断。 4) 文件放在 Web 无法直接访问的目录下。 5) 记录文件下载日志。 6) 不允许提供目录遍历服务。 7) 记录不符合规范的上传文件日志。 3.3.4.9【强制】 限制上传危险类型的文件 安全威胁 文件上传功能是基于 Web 业务系统的常见功能之一, 通常可以上传一些图片文件, 比如 jpeg 、pif 、png 等类型 。也可以上传一些文档类型的文件, 比如pdf、docx 等 。但是如果应用程序未对上传的文件类型进行限制的话, 可能会导致恶意文件被上传到服务器并造成危害等 。 比如上传 jsp 和 php 等类型的WebShell 文件, 进而导致服务器被控制权限等后果。解决方案 该漏洞危害程度较高, 在开发过程中要尽量避免发生 。处理用户上传文件,要做以下检查: 1) 检查上传文件扩展名白名单, 不属于白名单内, 不允许上传。 2) 上传文件的目录必须是 http 请求无法直接访问到的。如果需要访问的,必须上传到其他(和 Web 服务器不同的) 域名下, 并设置该目录为不解析 jsp 等脚本语言的目录。 3) 上传文件要保存的文件名和目录名由系统根据时间生成, 不允许用户自定义。 4) 图片上传, 要通过处理(缩略图 、水印等), 无异常后才能保存到服务器。 5) 上传文件需要做日志记录。 6) 3.3.4.10【强制】避免 Session 失效时间设置过长 安全威胁 Session 持续时间越长, 攻击者危害用户账户的机会就越大。 解决方案 上述代码片段中, 将 Session 失效时间设置为 30 min。 #### 3.3.5 Session 管理 3.3.5.1【强制】Cookie httponly flag 安全威胁 Cookie httponly, 是设置 COOKIE 时, 可以设置的一个属性, 如果 COOKIE没有设置这个属性, 该 COOKIE 值可以被页面脚本读取。当攻击者发现一个 XSS 漏洞时, 通常会写一段页面脚本, 窃取用户的COOKIE, 为了增加攻击者的门槛, 防止出现因为 XSS 漏洞导致大面积用户COOKIE 被盗, 所以应该在设置认证 COOKIE 时, 增加这个属性。解决方案 设置 cookie 时, 加入属性即可 常见问题 1) 在 cookie 类中没有找到设置 httponly 的方法? 目前的 jdk 版本只支持在 setHeader 时, 设置 httponly。 2) httponly 已经可以防止用户 cookie 被窃取, 还需要做 XSS 防御吗?这个 flag 只能增加攻击者的难度, 不能达到完全防御 XSS 攻击。 3.3.5.2【强制】Cookie Secure flag 安全威胁 Cookie Secure, 是设置 COOKIE 时, 可以设置的一个属性, 设置了这个属性后, 只有在 https 访问时, 浏览器才会发送该 COOKIE。浏览器默认只要使用 http 请求一个站点, 就会发送明文 cookie, 如果网络中有监控, 可能被截获。如果 Web 应用网站全站是 https 的, 可以设置 cookie 加上 Secure 属性,这样浏览器就只会在 https 访问时, 发送 cookie。攻击者即使窃听网络, 也无法获取用户明文 cookie。 解决方案 在设置认证 COOKIE 时, 加入 Secure。 再次访问 http 网站, 抓数据包可以看到, 已经不再发送这个 COOKIE 了。 图 3.3.10 3.3.5.3【强制】Session Expires 安全威胁 Session Expires, Session 有效期安全攻击。由于 Session 没有在 Web 应用中设置强制超时时间, 攻击者一旦曾经获取过用户的 Session, 就可以一直使用。解决方案 在设置认证 cookie 中, 加入两个时间, 一个是“ 即使一直在活动, 也要失效 ”的时间, 一个是“ 长时间不活动的失效时间 ”。 并在 Web 应用中, 首先判断两个时间是否已超时, 再执行其他操作。 #### 3.3.6 环境安全 3.3.6.1【强制】避免解析 Double 类型数据导致拒绝服务 安全威胁 程 序 调 用 Double 的 解 析 方 法 时 , 可 能 导 致 线 程 被 挂 起 。 java.lang.Double.parseDouble( ) 方 法 解 析 位 于 [2^(-1022) - 2^(-1075) : 2^(-1022) - 2^(-1076)]范围内的任何数字时可能导致线程被挂起, 攻击者可以故意触发该漏洞执行拒绝服务攻击 。该漏洞在 java6 update24 或更高版本中进行了修复。 解决方案 避免该缺陷的方式如下: 1) 验证传递给 parseDouble 数据的合法性。 2) 升级 JDK 版本到 6 Update 24 或更高版本。 3.3.6.2 【强制】避免 Struts2 的 S2-048 漏洞 安全威胁 Apache Struts2 是 Apache 基金会发布的一款实现了 MVC 模式的开源框架,广泛应用于 Web 开发和大型网站建设。使用了 Apache Struts 1 插件的 Apache Struts 2.3.X 版本中存在远程代码执行漏 洞(漏洞 编号: CNNVD-201706-928, CVE-2017-9791) 。 该 漏洞出 现 于 Struts2 的 Struts 1 插件的 org.apache.struts2.s1.StrutslAction 类中, 该类是为了将 Struts1 中的 Action 包装成为 Struts2 中的 Action , 以保证 Struts2 的兼容性。在 Struts2 中的 Struts1 插件启用的情况下, 远程攻击者可通过使用恶意字段值,构造特定的输入, 发送到 ActionMessage 类中, 从而导致任意命令执行, 进而获取目标主机系统权限。解决方案 ● 不要启用 Struts2-struts1-plugin 插件;● 不要 使用 showcase.war;● 始终使用资源键.而不是将原始消息传递给 ActionMessage:建议采用如下方式: ### 3.4 C/C++安全开发规范 #### 3.4.1 预处理器 3.4.1.1【强制】C/C++避免不安全宏的参数出现副作用 说明一个不安全的函数宏会在展开时多次使用或根本不使用某个参数 。绝对不要调用包含赋值 、增量 、 减量 、volatile 访问 、输入/输出等具有副作用(包括含有副作用的函数调用) 参数的不安全宏。若文档中涉及不安全宏的时候, 应当对使用含有副作用的参数调用进行警告, 但是责任根源在于程序员使用了某个不安全宏 。 由于在使用中会伴随风险, 应避免定义不安全的函数宏。实现方式不合规代码不安全宏的问题之一是其宏参数含有副作用, 正如下面这段不合规代码示例:#define ABS(x) (((x) < 0) ? __(x) : (x))void func(int n) {/* Validate that n is within the desired range */int m = ABS(++n);/* ... */ } 上面的 ABS()宏调用展开后如下:m = (((++n) < 0)? __(++n):(++n));生成的代码是明确的, 但会导致 n 递增两次而不是一次。合规代码在这个合规解决方案中, 增量操作++n 在宏调用之前得到执行。 #define ABS(x) (((x) < 0) ? __(x) : (x)) /* UNSAFE */ void func(int n) {/* Validate that n is within the desired range */++n;int m = ABS(n);/* ... */ } 注意上述代码在注释中警告了程序员该宏是不安全的, 该宏也可以改名为 ABS_UNSAFE()以明确告知宏是不安全的 。正如本规则的所有兼容解决方案, 如果 ABS()的参数等于最小的有符号整型值(最负值), 那么这个解决方案也具有未定义的行为。合规代码本合规解决方案遵守规范“PRE00C 以内联函数或静态函数代替类函数的宏 ”。不像 ABS()宏, 在对任何类型的操作数进行操作时, iabs()函数将截断比 int 类型更宽的参数, 这些参数的值范围不在 int 类型的范围内。#include #include static inline int iabs(int x) {return (((x) < 0) ? __(x) : (x)); }void func(int n) {/* Validate that n is within the desired range */int m = iabs(++n);/* ... */ } 合规代码更具有弹性的合规解决方案是让 ABS()宏使用_Generic 选项 。为了支持所有的数据类型, 此解决方案使用内联函数来计算整数绝对值。10 #### 3.4.2 C/C++声明和初始化 3.4.2.1【强制】声明具有正确存储持续期的对象 每 个 对 象 都 有 一 个 确 定 其 生 命 周 期 的 存 储 持 续 期: static 、 thread 、 automatic 或者 allocated。根据 C 标准 6.2.4 章节第 2 段【ISO/IEC 9899:2011】对象的生命周期就是能够保证存储持续有效的程序执行期间的部分时间,一个对象在它的生命周期内, 具有一个常量地址, 并且保留着最近一次所存储的值 。如果对象在其生命周期之外被引用, 则该行为是未定义的 。 当指针指向的对象生命周期结束时, 指针的值变得不确定。访问超出生命周期之外的对象是未定义的行为, 可导致产生可被利用的漏洞。不合规代码(不同的存储持续期)在这个不合规代码示例中, 变量 c_str 是 automatic 类型存储持续时间,被赋值给 static 类型存储持续时间变量 p, 赋值本身是有效的, 但是 c_str在超出 dont_do_this 函数之外就无效了, 而 p 还保存着它的地址。dont_do_this(void) { const char c_str[] = "This will change"; p = c_str; /* Dangerous */ } void innocuous(void) {printf("%s\n", p); }int main(void) { dont_do_this();innocuous(); return 0; }合规代码(相同的存储持续期)在这个解决方案中, p 被声明为和 c_str 一样的存储持续期, 这样就防止了 p 在 this_is_OK 函数之外值的不确定性。void this_is_OK(void) {const char c_str[] = "Everything OK";const char *p = c_str;/* ... */ } /* p is inaccessible outside the scope of string c_str */可替换方案, p 和 c_str 都声明为 static 类型存储持续期。合规代码(不同的存储持续期)如果 p 必须被声明为 static 类型存储持续期, 而 c_str 为受限制的存储持续期, 那么可以将 p 在 c_str 被销毁前设置为 NULL 。这样可以防止 p取不确定值, 但是任何引用 p 的地方都需要检查 p 是否为空。const char *p;void is_this_OK(void) {const char c_str[] = "Everything OK?";p = c_str;/* ... */p = NULL; }不合规代码(返回值)在这个示例中, 函数 init_array() 返回了一个指向字符数组的 automatic 类型存储持续期的指针。char *init_array(void) { char array[10];/* Initialize array */return array; }当函数返回 automatic 类型存储持续期的指针时, 一些编译器会产此,程序员应当在高警告级别上编译代码, 并解决所有告警。合规代码(返回值)在这个例子中, 这个解决方案取决于程序员的意图 。如果程序员的意图是修改 array 的值, 并且让这个修改在 init_array()函数作用域之外仍然得以保持, 可以通过在其他地方声明 array, 并把它作为参数传递给 init_array()来实现这个目的。#include void init_array(char *array, size_t len) {/* Initialize array */return; } int main(void) {char array[10];init_array(array, sizeof(array) / sizeof(array[0]));/* ... */return 0; }不合规代码(输出参数)在这个例子中, 函数 squirrel_away()存储了一个指向本地变量 local 的指针, 并 将 该 指 针 赋 值 给 函 数 入 参 ptr_param, 在 squirrel_away()返 回 后, ptr_param 指向的变量已经超出了生命周期。void squirrel_away(char **ptr_param) { char local[10];/* Initialize array */*ptr_param = local; }void rodent(void) {char *ptr;squirrel_away(&ptr);/* ptr is live but invalid here */ } 合规代码(输出参数)char local[10];void squirrel_away(char **ptr_param) {/* Initialize array */*ptr_param = local; }void rodent(void) {char *ptr;squirrel_away(&ptr);/* ptr is valid in this scope */ } 在这个例子中, 变量 local 具有 static 类型存储持续期, 所以 ptr 可以在rodent() 函数中引用 local 数组。 3.4.2.2【强制】不要声明具有冲突链接属性的标识符 在不同的作用域声明或同一个作用域中声明多次的标识符可以通过链接属性表示同一个对象或函数 。标识符的链接属性可以分为外部链接 、 内部链接 、无链接 。这三种链接属性有以下特性:外部链接: 具有外部链接属性的标识符在整个程序空间 (即在所有编译单元和库中) 表示相同的对象或函数 。标识符对链接器有效 。 当具有外部链接属性的相同标识符声明第二次出现时, 链接器会把这个标识符与同一个对象或函数相关联。内部链接: 具有内部链接属性的标识符在一个给定的翻译单元中表示同一个对象或函数 。链接器没有内部链接属性标识符相关的信息, 因此, 这类标识符是属于翻译单元内部的。无链接: 如果标识符的链接属性为无链接, 则使用这个标识符的所有未来声明都是新的, 如新变量或新类型。根据 C11 标准 6.2.2 节, 链接属性是按照下面方式确定的:如果一个具有文件作用域的表示对象或函数的标识符声明中包含了存储类指示符 static, 这个标识符就具有内部链接属性。如果一个标识符在声明时使用了存储类型指示符extern, 并且在它的作用域中已经有了这个标识符的以前声明, 如果以前声明指定了内部或外部链接属性, 这个标识符后来的声明的链接属性就与之前声明的相同 。如果作用域中没有这个标识符的以前声明, 或者它以前声明的链接属性为无链接, 这个标识符就具有外部链接属性。如果一个表示函数的标识符声明没有使用存储类型指示符extern, 它的链接属性就像使用了存储类型指示符 extern 声明的一样, 如果一个表示对象的标识符具有文件作用域, 并且没有存储类型指示符, 它的链接属性就是外部链接。下面这类标识符的链接属性为无链接: 被声明为不是函数或对象的标识符 、被声明为函数形参的标识符 、被声明为对象但没有使用存储类型指示符 extern 的代码块作用域中的标识符。 在一个翻译单元内部, 一个标识符既具有内部链接属性又具有外部链接属性将会导致未定义的行为 。 (见未定义行为 8) 翻译单元包括通过预处理指令#include 包含的所有头文件和源文件。下面这个表说明了在一个翻译单元内一个对象在被第二次声明时的链接属性 。列代表第一次声明, 行代表重复声明。不合规代码在这个不合规例子中, i2 和 i5 被定义为同时具有内部和外部链接属性,使用这两个标识符将会导致未定义的行为。int i1 = 10; /* Definition, external linkage */static int i2 = 20; /* Definition, internal linkage */extern int i3 = 30; /* Definition, external linkage */int i4; /* Tentative definition, external linkage */ static int i5; /* Tentative definition, internal linkage */ int i1; /* Valid tentative definition */int i2; /* Undefined, linkage disagreement with previous */ int i3; /* Valid tentative definition */int i4; /* Valid tentative definition */int i5; /* Undefined, linkage disagreement with previous */ int main(void) { /* ... */return 0; }微软 Visual Studio2013 对上述情况不会产生告警, 即便是设置了最高告警级别。GCC 编译器会为 i2 和 i5 定义冲突产生一个致命诊断信息。合规代码int i1 = 10; /* Definition, external linkage */ static int i2 = 20; /* Definition, internal linkage */ extern int i3 = 30; /* Definition, external linkage */ int i4; /* Tentative definition, external linkage */ static int i5; /* Tentative definition, internal linkage */ int main(void) { /* ... */return 0; }3.4.2.3【强制】不要创建与同一个函数或对象不兼容的声明 同一个函数或对象的两个或多个不兼容的声明不能出现在同一程序中,因为可能导致未定义的行为 。在 C 标准 6.2.7 节中提到了两种类型可能不同但是是兼容的, 这种情况可以被准确处理。C 标准确定, 由于相同函数或对象的不兼容声明, 可能导致四种情况未定义行为出现: 虽然出现在同一程序里两个不兼容的声明在大多数应用里可能是良性的,但是通过与函数定义类型不兼容的表达式调用函数会产生灾难性的后果 。 同样, 用与对象类型不兼容的类型访问对象可能导信息泄露 、 内存覆盖或硬件陷阱。不合规代码(不兼容的对象声明)在这个不合规的代码示例中, 变量 i 在文件 a.c 中声明为 int 类型, 但在文件 b.c 中却定义为 short 类型 。这两个声明是不兼容的, 会导致未定义行为 15 。此外, 在函数 f() , 使用不兼容的左值类型获取对象数据, 会导致未定义行为 37, 有可能导致信息暴露 、 内存覆盖或硬件陷阱。/* In a.c */extern int i; /* UB 15 */ int f(void) {return ++i; /* UB 37 */ }/* In b.c */short i; /* UB 15 */合规代码(不兼容的对象声明)/* In a.c */extern int i; int f(void) { return ++i; }/* In b.c */int i;不合规代码(不兼容的数组声明)在这个不合规的代码示例中, 变量 a 在文件 a.c 中被声明为指针类型,但在文件 b.c 中却定义为数组类型 。这两个声明是不兼容的, 导致了未定义行为 15 。像之前一样, 在函数 f()中访问对象会导致未定义行为 37, 有可能导致信息暴露 、 内存覆盖或硬件陷阱。/* In a.c */ extern int *a; /* UB 15 */ int f(unsigned int i, int x) {int tmp = a[i]; /* UB 37: read access */ a[i] = x; /* UB 37: write access */ return tmp; } /* In b.c */int a[] = { 1, 2, 3, 4 }; /* UB 15 */合规代码(不兼容的数组声明)/* In a.c */extern int a[];int f(unsigned int i, int x) {int tmp = a[i];a[i] = x;return tmp; }/* In b.c */int a[] = { 1, 2, 3, 4 };不合规代码(不兼容的函数声明)在这个不合规代码示例中, 函数 f()在文件 a.c 中声明的原型与在b.c 中定义的类型不一致 。这两个原型是不兼容的, 导致未定义行为 15 。此外, 调用该函数是未定义行为 41, 通常具有灾难性的后果。/* In a.c */extern int f(int a); /* UB 15 */ int g(int a) { return f(a); /* UB 41 */ } /* In b.c */long f(long a) {return a * 2; /* UB 15 */ }合规代码(不兼容的函数声明)/* In a.c */extern int f(int a);int g(int a) {return f(a); }/* In b.c */int f(int a) {return a * 2; }不合规代码(不兼容的可变函数声明)在这个不合规代码示例中, 函数 buginf()被定义为含有可变数量的参数,并期望它们都是有符号整数, 并以 1 结尾。/* In a.c */void buginf(const char *fmt, ...) {/* ... */ } /* In b.c */void buginf();虽然这段代码中buginf() 函数定义好像没有问题, 只是函数原型声明中缺少了些信息, 但按照 C11 标准 6.7.6.3 节第 15 段该代码会表现出未定义行为。 两个函数类型如果是兼容的, 两者应指定兼容的返回类型 。此外, 如果两者都有参数列表, 应该在参数个数和省略终止符上一致, 对应参数类型应该互相兼容。合规代码(不兼容的可变函数声明)/* In a.c */void buginf(const char *fmt, ...) {/* ... */ } /* In b.c */void buginf(const char *fmt, ...);不合规代码(超长标识符)在这个不合规代码示例中, 在文件 bashline.h 中声明函数指针的标识符bash_groupname_completion_function() 长度超过了外部标识符最长为 31 的限制。这引入了与在 b.c 中定义的整型变量 bash_groupname_completion_funct 碰撞的可能性, 它的长度恰好是 31 个字符 。对于恰好满足此限制的应用, 这属于[未定义行为 31] 。这是同一个函数的两个不兼容的声明 。此外, 调用该函数将导致未定义行为 41, 产生典型的灾难性结果。/* In bashline.h *//* UB 15, UB 31 */extern char * bash_groupname_completion_function(const char *, int);/* In a.c */#include "bashline.h"void f(const char *s, int i) {bash_groupname_completion_function(s, i); /* UB 41 */ }/* In b.c */int bash_groupname_completion_funct; /* UB 15, UB 31 */ 合规代码(超长标识符)在 这 个 合 规 解 决 方 案 中, 在 bashline.h 中 声 明 的 函 数 指 针 标 识 符bash_groupname_completion() 长 度 少 于 32 个 字 符 。 因 此 , 它 不 会 与bash_groupname_completion_funct 在任何兼容平台上产生冲突。/* In bashline.h */extern char * bash_groupname_completion(const char *, int);/* In a.c */#include "bashline.h"void f(const char *s, int i) { bash_groupname_completion(s, i); }/* In b.c */int bash_groupname_completion_funct; #### 3.4.3 C/C++表达式 3.4.3.1【强制】不允许对 null 指针进行解引用 对 null 指针进行解引用是未定义行为(undefined behavior)。在许多平台上, 解引用null 指针会导致程序异常终止, 但是标准中没有明确要求这一点。见"Clever Attack Exploits FullyPatched Linux Kernel" [Goodin 2009]。不合规代码该不合规代码示例来自于一个真实的例子, 原为部署于某种 ARM 手机上的 libping 库漏洞版本 。 libpng 库允许应用读取 、创建和操作 PNG 图片文件 。该库使用自己封装的 malloc()函数, 当遇到长度为 0 或者其他错误时返回 null 指针 。该代码同时也违反了 ERR33C 检查和处理标准库错误。#include /* From libpng */#include void func(png_structp png_ptr, int length, const void *user_data) { png_charp chunkdata;chunkdata = (png_charp)png_malloc(png_ptr, length + 1);/* ... */memcpy(chunkdata, user_data, length);/* ... */ } 如果 length 的值为 1, 加法的得数为 0, png_malloc()将返回 null 指针并赋值给 chunkdata 。 随后 chunkdata 作为 memcpy()的目的参数, 导致用户定义的数据覆盖地址 0 开始的内存 。在 ARM 和 XScale 的架构中, 0x0 地址映射到内存中并作为异常向量表, 因此, 解引用 0x0 不会导致程序异常终止。合规代码该例中的解决方案是确保 png_malloc()返回的指针不为 null, 同时还通过使用无符号 size_t 来传递 length 参数, 确保不会向 func()传递负值。#include /* From libpng */#include void func(png_structp png_ptr, size_t length, const void *user_data) {png_charp chunkdata;if (length == SIZE_MAX) {/* Handle error */ } chunkdata = (png_charp)png_malloc(png_ptr, length + 1); if (NULL == chunkdata) {/* Handle error */ } /* ... */memcpy(chunkdata, user_data, length);/* ... */ } 不合规代码 在该不合规代码示例中, input_str 被拷贝到 c_str 指向的动态分配内存 。如果 malloc() 失败, 那 c_str 被赋值为 null 指针 。 当 memcpy()解引用 c_str时, 程序出现未定义行为 。另外, 如果 input_str 是 null 指针, 调用 strlen()解引用 null 指针, 也会导致未定义行为 。这段代码同时还违反了 ERR33C检查和处理标准库错误。#include #include void f(const char *input_str) {size_t size = strlen(input_str) + 1;char *c_str = (char *)malloc(size);memcpy(c_str, input_str, size);/* ... */free(c_str);c_str = NULL;/* ... */ } 合规代码该合规解决方案是要确保 input_str 和 malloc()返回的指针不是 null:#include #include void f(const char *input_str) {size_t size;char *c_str;if (NULL == input_str) {/* Handle error */ } size = strlen(input_str) + 1; c_str = (char *)malloc(size);if (NULL == c_str) {/* Handle error */ } memcpy(c_str, input_str, size);/* ... */free(c_str);c_str = NULL;/* ... */ } 不合规代码该 不 合 规 代 码 示 例 来 自 于 drivers/net/tun.c 的 某 个 版 本 , 并 且 对Linux Kernel 2.6.30 造成了影响。static unsigned int tun_chr_poll(struct file *file, poll_table *wait) {struct tun_file *tfile = file __>private_data;struct tun_struct *tun = tun_get(tfile);struct sock *sk = tun __>sk;unsigned int mask = 0;if (!tun)return POLLERR;DBG(KERN_INFO "%s: tun_chr_poll\n", tun __>dev __>name);poll_wait(file, &tun __>socket.wait, wait);if (!skb_queue_empty(&tun __>readq))mask |= POLLIN | POLLRDNORM; if (sock_writeable(sk) ||(!test_and_set_bit(SOCK_ASYNC_NOSPACE, &sk __>sk_socket __>flags) && sock_writeable(sk)))mask |= POLLOUT | POLLWRNORM; if (tun __>dev __>reg_state != NETREG_REGISTERED)mask = POLLERR; tun_put(tun);return mask; }指针 sk 在检查 tun 是否为空之前就被初始化为 tun __>sk 。 由于解引用 null 指针是未定义行为, 编译器(在该场景下为 GCC) 可能在优化中去掉if (!tun)检查, 因为它在 tun __>sk 之后才执行, 这也就意味着 tun 是非空的。因此, 这段代码示例容易遭到 null 指针解引用攻击, 因为在多个平台上可能允许 null 解引用, 例如在 Linux 和 Mac OS X 上使用带有 MAP_FIXED标志的 mmap(2), 或者使用带有 SHM_RND 标志的_shmat()POSIX 函数。合规代码将 sk 的初始化放到 null 指针检查之后。static unsigned int tun_chr_poll(struct file *file, poll_table *wait) {struct tun_file *tfile = file __>private_data;struct tun_struct *tun = tun_get(tfile);struct sock *sk;unsigned int mask = 0;if (!tun)return POLLERR; sk = tun __>sk;/* The remaining code is omitted because it is unchanged... */ } 3.4.3.2【强制】调用函数时使用正确的参数数目和类型 不要用错误的参数数目或类型来调用函数。 C 语言标准确定了 5 种不同情况, 在这 5 种情况中, 调用函数声明与其定义不符 、 提供参数类型或者数量错误可能造成未定义行为(undefined behavior, UB):正常声明的函数如果得到错误数量或者类型的参数, 通常会产生一条编译程序诊断消息 。然而, 有些情况下向函数提供不正确的参数最多只会产生编译程序警告 。尽管这些警告应该得到解决, 但是它们不会阻止程序编译。 (参见“MSC00C. Compile cleanly at high warninglevels ”)不合规代码 头文件为数学函数提供泛型宏 。尽管头文件 中的大部分函数在 中有等价的复数版本, 但是仍有部分函数没有 。使用复数来调用下表中的泛型函数就是未定义行为。Functions That Should Not Be Called with Complex Values 示例中试图用log2() 函数对复数进行计算, 导致了未定义行为:#include void func(void) { double complex c = 2.0 + 4.0 * I;double complex result = log2(c); }合规代码如果函数 clog2()不能扩展到复数, 程序员可以使用 log()来代替 log2()实现对复数求对数, 因为 log()可以使用复数作为参数, 例如下面的代码:#include void func(void) { double complex c = 2.0 + 4.0 * I;double complex result = log(c)/log(2); }合规代码(Real Number)如果程序员的意图是对复数的实数部分求以 2 为底的对数, 可以使用下面的方案:#include void func(void) { double complex c = 2.0 + 4.0 * I;double complex result = log2(creal(c)); }不合规代码在该不合格示例中, 通过函数指针 fp 来调用 C 语言标准库函数 strchr(),但是 fp 声明的原型中参数类型不正确 。 根据 C 标准 6.3.2.3 第 8 段 [ISO/IEC 9899:2011]: 指向某个类型的函数的指针可能转换为指向另一个类型函数的指针, 然后复原, 其结果应该与原始指针相同 。如果转换后的指针用来调用与引用类型不符的函数, 其行为就是未定义的。见未定义行为 26(undefined behavior 26)。#include #include char *(*fp)();int main(void) {const char *c;fp = strchr;c = fp('e', "Hello");printf("%s\n", c);return 0; }合规代码在该合规解决方案中, 函数指针 fp 指向 C 语言标准库函数 strchr(), 声明了正确的参数, 并且用正确数量和类型的实际参数调用:#include #include char *(*fp)(const char *, int);int main(void) {const char *c;fp = strchr;c = fp("Hello",'e');printf("%s\n", c);return 0; }不合规代码 在该示例中, 函数 f()定义为使用 long 类型的参数, 但是在另一文件中调用f() 时使用了 int 类型的参数:/* In another source file */long f(long x) {return x < 0 ? _x : x; }/* In this source file, no f prototype in scope */long f();long g(int x) {return f(x); }合规代码在该合规解决方案中, 函数 f()的原型包含在被调用的作用域的源文件中,而且用 long 类型的参数正确调用函数:/* In another source file */long f(long x) {return x < 0 ? _x : x; }/* f prototype in scope in this source file */long f(long x);long g(int x) {return f((long)x); }不合规代码(POSIX)POSIX 函数 open() [IEEE Std 1003.1:2013]是可变参数函数, 原型如下: int open(const char *path, int oflag, ... ); open()函数接受的第三个参数, 用来确定新创建文件的访问模式 。 如果open()用于创建新文件, 且第三个参数被忽略, 则创建的文件可能具有意外的访问权限 。(参见 FIO06C. Create files with appropriate access permissions) 该示例来自 CVE20061174 shadowutils 包的 useradd()函数中的一个漏洞, open()的第三个参数被意外地忽略了:fd = open(ms, O_CREAT | O_EXCL | O_WRONLY | O_TRUNC);注意, 从技术上说, 在不创建新文件时(也就是不设置 O_CREAT 标志),向 open()传递第三个参数是不正确的。合规代码(POSIX)在该解决方案中, 调 open() 时指定第三个参数:#include void func(const char *ms, mode_t perms) {/* ... */int fd;fd = open(ms, O_CREAT | O_EXCL | O_WRONLY | O_TRUNC, perms); if (fd == ‐ 1) {/* Handle error */ } } 3.4.3.3【强制】不允许修改常量对象 C 标准 6.7.3 第 6 [ISO/IEC 9899:2011], 规定:如果试图通过非 const 限定的左值来修改 const 限定的对象, 其行为就是未定义的。见未定义行为 64(undefined behavior 64)。有些编译器允许修改 const 限定的对象, 而不会产生告警。 要避免转换掉 const 限定符, 因为这会导致修改 const 限定的对象而不产生诊断告警 。(更多细节见 EXP05C. Do not cast away a const qualification, 以及 STR30C. Do not attemptto modify string literals )不合规代码该不合规代码示例允许修改常量对象: const int **ipp;int *ip;const int i = 42;void func(void) {ipp = &ip; /* Constraint violation */*ipp = &i; /* Valid */*ip = 0; /* Modifies constant i (was 42) */ }第一条赋值语句是不安全的, 因为它允许后续的代码尝试修改常量对象 i的值。如果 ipp , ip 和 i 声明为自动变量, 该例在 Microsoft Visual Studio 2013环境下以 C 模式编译不产生告警, 且程序结果是修改了 i 的值 。GCC 4.8.1编译时产生了告警, 但程序结果还是修改了 i 的值。如 果 ipp , ip 和 i 声 明 为 静 态 存 储 变 量 , 该 程 序 在Microsoft Visual Studio 2013 环境中不会产生告警并异常终止, 在 GCC 4.8.1中编译产生告警并异常终止。合规代码合规解决方案取决于程序员的意图 。如果认 i 是可修改的, 则解决方案是不应把 i 声明为常量:int **ipp;int *ip;int i = 42;void func(void) {ipp = &ip; /* Valid */ *ipp = &i; /* Valid */*ip = 0; /* Valid */ } #### 3.4.4 C/C++整数 3.4.4.1【强制】保证无符号整型数操作不会出现回绕 根据 C 语言标准 6.2.5 节第 9 段 ISO/IEC 9899:2011 所述:涉及无符号整数的计算不会溢出, 因为无法由最终的无符号整数类型表示的结果将会根据这种最终类型可以表示的最大值加 1 执行求模操作。以上行为更为正式的名称是无符号整数回绕(unsigned interger wrapping)。如果结果值无法由整数的底层表示形式表示, 就会产生回绕 。下表中给出了哪些操作符可能会产生回绕。下面各节讨论了可能会产生无符号整数回绕的特定操作 。对小整数类型(小于 int) 进行操作时, 会发生整型提升 。另外, 在执行运算之前, 也可能会发生算术转换, 即将操作数(隐式的) 转换为相同的类型 。 因此, 在实 现 安 全 的 算 术 运 算 之 前 , 要 确 保 理 解 整 数 转 换 规 则 ( 参见 INT02C. Understand integer conversion rules)。以下列方式使用整数时, 尤其不应该出现整数回绕的情况:在任何指针运算中, 包括作为数组索引用于声明变长数组时;通过自增或自减运算得到数组起始地址, 或用于指定数组下标的表达式;作为 size_t 或 rsize_t 类型的实参(比如内存分配函数的参数);在安全关键的代码中 C 语言标准对于原子整数类型的算术运算, 定义了与普通整数类型同样的表示方式 。 因此, 对于原子整数类型, 也应该如同普通整数类型一样对回绕的情况进行检查和预防。加法加法是在两个算术类型的操作数之间或一个指向对象的指针和一个整数类型之间执行的 。本规则只应用于两个算术类型相加的情况(指针加法的规则参见 ARR37C. Do not add orsubtract an integer to a pointer to a nonarray object 及 ARR30C. Do not form or use outofbounds pointers or array s ubscripts)。此外, 自增运算等同于加 1 运算。不合规代码在以下的不合规代码示例中, 无符号的算子 ui_a 和 ui_b 相加时可能会产生无符号整型回绕 。如果回绕行为是非预期的(unexpected), 那么就可能会导致诸如分配了不足额的内存供后续操作使用, 或易受攻击的安全漏洞(vulnerability)。void func(unsigned int ui_a, unsigned int ui_b) {unsigned int usum = ui_a + ui_b;/* ... */ } 合规代码这种, void func(unsigned int ui_a)方案对参与相加的操作数进行了前置条件测试, 以保证不会出现回绕。#include void func(unsigned int ui_a, unsigned int ui_b) {unsigned int usum;if (UINT_MAX _ ui_a < ui_b) {/* Handle error */ } else {usum = ui_a + ui_b; }/* ... */ } 合规代码(后置条件测试)这种方案对相加运算进行了后置条件测试, 如果相加后得到的 usum 不小于第一个操作数, 则说明没有发生回绕。void func(unsigned int ui_a, unsigned int ui_b) {unsigned int usum = ui_a + ui_b;if (usum < ui_a) {/* Handle error */ } /* ... */ } 减法减法是在两个算术类型的操作数之间, 两个指向兼容对象类型的限定或未限定版本的指针之间, 或一个指向一种对象类型的指针与一个整数类型的值之间执行的 。本规则只应用于两个算术类型的操作数之间的减法(关于指针运算参见 ARR36C. Do not subtract or compare two pointers that do not refer to the same array, ARR37C. Do not add or subtract an integer to a pointer to a nonarray object 及 ARR 30C. Do not form or use outofbounds pointers or array subscripts)。此外, 自减运算等同于减 1 运算。不合规代码在以下的不合规代码示例中, 无符号的算子 ui_a 和 ui_b 相减时可能会产生无符号整数回绕 。如果回绕行为是非预期的, 那么就可能导致易受攻击的安全漏洞。void func(unsigned int ui_a, unsigned int ui_b) {unsigned int udiff = ui_a - ui_b;/* ... */ } 合规代码这种方案对参与相减的操作数进行了前置条件测试, 以保证不会出现回绕。#include void func(unsigned int ui_a, unsigned int ui_b) {unsigned int udiff;if (ui_a < ui_b) {/* Handle error */ } else {udiff = ui_a _ ui_b; }/* ... */ } 合规代码(后置条件测试)这种方案对减法运算进行了后置条件测试, 如果相减的结果 udiff 不大于被减数, 则说明没有发生回绕。void func(unsigned int ui_a, unsigned int ui_b) {unsigned int udiff = ui_a _ ui_b; if (udiff > ui_a) { /* Handle error */ } /* ... */ } 乘法乘法是在两个算术类型的操作数之间执行的。不合规代码在 Mozilla Scalable Vector Graphics (SVG)的查看程序中曾经存在一个堆缓冲区溢出的漏洞, 这是由于 signed int 类型的值 pen>num_vertices 与 size_t类型的值 sizeof(cairo_pen_vertex_t) 相乘造成的 VU#551436] 。在进行乘法运算之前, signed int 类型的操作数首先会被转换成无符号的 size_t 类型 (参见 INT02C. Understand integer conversion rules)。pen __>vertices = malloc( pen __>num_vertices * sizeof(cairo_pen_vertex_t) );无符号整数回绕可能会导致分配到的内存不足。合规代码(后置条件测试)if (pen __>num_vertices > SIZE_MAX / sizeof(cairo_pen_vertex_t)) {/* Handle error */ } pen __>vertices = malloc( pen __>num_vertices * sizeof(cairo_pen_vertex_t) );合规解决方案是测试乘法的操作数, 以保证不会出现回绕。 例外INT30CEX1: 在符合程序执行的需要时, 无符号整数也可以表现出取模(回绕) 行为 。建议对于支持取模行为的整数, 在声明变量以及对它的每一次操作时, 都使用注释明确说明。INT30CEX2: 当在编译期就能够确定不会发生回绕行为时, 那么就可以省略掉对回绕的检查 。 比如以下对无符号整数的运算操作就可以免去回绕检查:两个编译期常量之间的运算变量与 0 之间的运算(除了除 0 或对 0 取余的运算之外)减法运算中, 以减数的类型的最大值作为被减数时; 比如, 从UINT_MAX 减去任意的 unsigned int 类型的数都是安全的任何变量与 1 相乘因子非 0 时的除法和取余运算向右移位的位数不超过被操作的整数的精度时;比如对于 UNIT_MAX >> x 操作, 只要 x 满足 0 <= x < 32 就是安全的 (假设 unsigned int 类型的精度是 32 位)。INT30CEX3: 两个整数之间的向左移位操作 。对无符号整数的左移操作 << 可以有取模(回绕) 行为 。之所以允许这种例外, 是因为通常情况下这是程序员本身所期望的, 并且移位操作也有明确的规范定义 (参见 INT34C 不要将表达式移动负数位或移动大于等于操作数中存在的位数)。3.4.4.2【强制】保证整型转换不会丢失或错误解释数据 整型转换(包括隐式转换和显示转换) 必须保证不会丢失或错误解释数据 。对于来自不信任来源并且按以下方式使用的数据, 这一点尤其重要:在任何指针运算中, 包括作为数组索引用于声明变长数组时通过自增或自减运算得到数组起始地址, 或用于指定数组下标的表达式作为 size_t 或 rsize_t 类型的实参(比如内存分配函数的参数)本规则也被应用于传递给以下标准库函数的参数, 这些参数会被转换为unsigend char 类型:memset() fprintf() 及类似函数 (对于格式化修饰符 %c , 在不与修饰符%l 组合使用时, 整数参数会被转换为一个 unsigned char 类型的值, 并且作为一个字符写入目标流中)fputc()ungetc()memchr()此外, 传递给以下库函数的参数会被转换为char 类型:strchr()strrchr() 中列举的所有函数对于所有的数据值以及所有遵循标准的编译器, 唯一能保证安全的整数类型转换是将一个整数值转换为一种具有相同符号的更宽的类型 。C 语言标准 6.3.1.3 ISO/IEC 9899:2011 表示:当一个整数类型的值被转换为除_Bool 以外的另一种整数类型时, 如果这个值能够用新类型表示, 它就不会被修改。否则, 如果新类型是无符号的, 这个值就反复加上或减去“这种新类型可以表示的最大值加 1 ”, 直到这个值位于新类型的范围之内。否则, 这种新类型是有符号的并且这个值无法被新类型表示, 此时它的结果是由编译器定义的, 或者产生一个基于编译器实现的信号。一般情况下, 将一个整数类型转换为一个更小的类型会导致高位被截断。不合规代码(无符号转为有符号)从一种无符号类型的转换为一种有符号类型时, 可能会发生类型范围错误, 包括数据丢失(截断) 和符号位丢失(符号错误)。下面的不合规代码在大多数的编译器中都会产生一个截断错误:#include void func(void) {unsigned long int u_a = ULONG_MAX;signed char sc;sc = (signed char)u_a; /* Cast eliminates warning */ /* ... */ } 合规代码(无符号转为有符号)从一种无符号类型转换为一种有符号类型时对范围进行验证 。 例如以下的代码可以用于从 unsigned long int 型转换为 signed char 类型:#include void func(void) { unsigned long int u_a = ULONG_MAX;signed char sc;if (u_a <= SCHAR_MAX) {sc = (signed char)u_a; /* Cast eliminates warning */ } else {/* Handle error */ } } 不合规代码(有符号转为无符号)从一种有符号类型的转换为一种无符号类型时, 可能会发生类型范围错误, 包括数据丢失(截断) 和符号位丢失(符号错误)。下面的不合规代码会导致符号位丢失:#include void func(void) { signed int si = INT_MIN; /* Cast eliminates warning */ unsigned int ui = (unsigned int)si;/* ... */ } 合规代码(有符号转为无符号)从一种有符号类型转换为一种无符号类型时对范围进行验证 。 例如以下的代码可以用于 signed int 类型转换为 unsigned char 类型:#include void func(void) { signed int si = INT_MIN;unsigned int ui;if (si < 0) {/* Handle error */ } else {ui = (unsigned int)si; /* Cast eliminates warning */ }/* ... */ } 根据 C 语言标准 ISO/IEC 9899:2011 6.2.5 第 9 段的条款, 遵循标准的编译器实现(conforming implementation) 能够保证以上的方案有效。有符号整数类型的非负值的范围是相应的无符号整数类型的子范围, 并且两种类型中相同值的表示是相同的。不合规代码(有符号数转换, 精度损失)从一种有符号整数类型转换为另一种精度更小的有符号类型时, 可能会发生数据损失(截断)。 以下的不合规代码在大多数编译器上都会发生数据截断错误:#include void func(void) { signed long int s_a = LONG_MAX; signed char sc = (signed char)s_a; /* Cast eliminates warning *//* ... */ } 合规代码(有符号数转换, 精度损失)从一种无符号类型转换为一种精度更小的无符号类型时, 对范围进行验证 。 例如以下的代码可以用于从 unsigned long int 类型转换为 unsigned char 类型:#include void func(void) { unsigned long int u_a = ULONG_MAX;unsigned char uc;if (u_a > UCHAR_MAX) {/* Handle error */ } else {uc = (unsigned char)u_a; /* Cast eliminates warning */ }/* ... */ } 从无符号整数类型转换为精度更小的另一种无符号类型时, 只有上界需要检查。不合规代码(time_t 类型作为返回值)time() 函数在表示当前日历时间无效时, 会将 1 转换为 time_t 类型后的值(即 (time_t)( __1) ) 返回 。C 语言标准只要求 time_t 类型是一个能够表示时间的实数类型(整数和实数浮点数类型统称为实数类型)。具体采用哪种实数类型表示时间, 交由编译器实现来决定 。 如果 time_t 被实现为一个精度小于 signed int 的无符号整数类型, 那么 time() 的返回值将永远不会与整型字面常量 一1 相等。#include void func(void) { time_t now = time(NULL);if (now != 一1) {/* Continue processing */ } } 合规代码(time_t 类型作为返回值)为了确保比较正确执行, time() 的返回值应该与 一1 被转换成 time_t 类型以后的值比较:#include void func(void) { time_t now = time(NULL);if (now != (time_t) 一1) {/* Continue processing */ } } 这个解决方案同样也符合INT18C. Evaluate integer expressions in a larger size before comparing or assigning t o that size 的规定。不合规代码(memset())因为历史原因, 某些 C 标准库函数可以接受一个 int 类型的参数并将其转换为unsigned char 类型或普通的 char 类型 。如果参数的值无法被更小的类型 表示, 这种转换可能会导致非预期的行为 。 以下的不合规代码会将数组置为全 0,这显然是不符合预期的:#include #include int *init_memory(int *array, size_t n) {return memset(array, 4096, n); }合规代码(memset())一般来说, memeset() 函数不应该用于初始化整数数组, 除非是希望将整个数组置0:#include #include int *init_memory(int *array, size_t n) {return memset(array, 0, n); }例外:INT31CEX1: C 语 言 标 准 定 义 了 标 准 整 数 类 型 的 最 小 范 围 。 例 如, unsigned short int 类型对象的最小范围是 0 ~ 65,535, 而 int 类型的最小范围是 32,767 ~ +32,767 。这意味着 unsigned short int 类型的值并不都能够用 int 来表 示 。 但 是 , 在 IA32 架 构 中 , 实 际 的 整 数 范 围是 一2,147,483,648 ~ +2,147,483,647 , 这 意 味 着 在 这 种 架 构 下 所 有的 unsigned short int 值都可以用 int 类型来表示 。 因此, 在 IA32 架构下对这种转换进行测试就是不必要的 。而在不知道底层类型的精度的情况下, 无法对转换行为作出假设 。如果未提供对转换的测试, 就必须明确记录与精度有关的假设 。在这些假设无效的系统中, 这些代码就无法安全的移植 。记录这些 假 设 的 一 种 较 好 的 方 式 是 使 用 静 态 断 言 ( 参 见DCL03C. Use a static assertion to test the value of a constant expression)。 INT31CEX2: 只要值表示一个字符, 而不是一个整数, 允许从任何整数类型转换为值为 SCHAR_MIN 和 UCHAR_MAX 之间的字符类型。转换为无符号字符类型由 C 定义为模块化行为 。字符的值不会由于符号的丢失或转换为负数而被曲解 。 例如, 欧元符号 € 有时由位模式 0x80 表示,根据类型的符号性, 其可以具有数值 128 或 一127 。转 换 为 有 符 号 字 符 类 型 更 容 易 出 现 问 题 。 C 标 准 6.3.1.3 第 3 段(ISO/IEC 9899:2011) 的条款表示, 关于有符号类型转换:否则, 这种新类型是有符号的并且这个值无法被新类型表示, 此时它的结果是由编译器定义的, 或者产生一个基于编译器实现的信号。此外, 6.2.6.2 第 2 段的条款表示, 关于此时的整型变换:如果符号位是 1, 这个值应该按以下的方式之一进行变换:符号位 0 表示负值(原码)符号位的值为 (2M)(补码)符号位的值为 (2M 1) (反码)以上情况中, 当符号位为 1 而其它位为 0 时(对于前两种情况), 或者当符号位和其它位都为 1 时(对于反码的情况), 由编译器实现来决定属于正常值还是异常表示。因此, 标准允许以下代码陷入系统中断:int i = 128; /* 1000 0000 in binary */ assert(SCHAR_MAX == 127); signed char c = i; /* can trap */但是, 会将这段代码处理为异常或者产生一个无法预料的值的平台是很少见的 。根据 DerkJones 在The New C Standard:An Economic and Cultural Commentary 中所述:具有这种处理方式的实现过去被认为是存在的, 但是无法找到任何描述这类处理器的文档。3.4.4.3【强制】保证有符号整数的运算不会溢出 有符号整数的溢出是一种未定义行为, 这意味着编译器在处理有符号整数的溢出时有多种选择(参见 MSC15C. Do not depend on undefined behavior)。 例如, 将有符号整数类型定义为具有取模行为的编译器就不需要检查整数溢出 。编译器也可以捕捉有符号算术溢出, 或者简单的假设溢出永远不会发生,并相应的生成代码 。 同一个遵循标准的编译器在不同的场景下生成具有不同行为的代码, 也同样是可能的 。 例如, 一个编译器可能判定在局部范围声明的有符号整型的循环控制变量不可能溢出, 并基于这一点来生成有效的代码;而同一个编译器也可能判定在类似场景下使用的全局变量将会捕捉到异常。基于这些理由, 保证有符号整型运算不会导致溢出是非常重要的 。尤其是来自于不信任源(tainted source) 的数据按照以下方式被使用时:在任何指针运算中, 包括作为数组索引用于声明变长数组时通过自增或自减运算得到数组起始地址, 或用于指定数组下标的表达式作为 size_t 或rsize_t 类型的实参(比如内存分配函数的参数)如果结果值无法由整数的底层表示形式表示, 整数运算就会发生溢出。下表中给出了哪些操作符可能会产生溢出。下面各节讨论了可能会产生有符号整数溢出的特定操作 。对小整数类型(小于 int ) 进行操作时, 会发生整型提升 。另外, 在执行运算之前, 也可能会发生算术转换, 即将操作数(隐式的) 转换为相同的类型 。 因此, 在实 现 安 全 的 算 术 运 算 之 前 , 要 确 保 理 解 整 数 转 换 规 则 ( 参见 INT02C. Understand integer conversion rules)。实现细节使用 一fwrapv 命令行选项调用的 GNU GCC 编译器为无符号的和有符号的整数定义了同样的取模算法。当一个有符号整数溢出时, 使用 一ftrapv命令行选项调用的 GNU GCC 编译器会产生一个陷阱, 这很有可能导致异常退出 。在 UNIX 系统上, 这个事件可能会产生一个发送到进程的信号。既没有 一fwrapv 选项也没有 一ftrapv选项调用的 GNU GCC 编译器, 只是简单的假设有符号整数绝对不会溢出, 并生成相应的目标代码。原子整数C 标准定义了有符号原子整数的算术行为, 以便在溢出时使用静默回绕的补码表示 。尽管有定义, 但是这些结果仍然可能是非期望的, 并且会因此带来与无符号整数回绕类似的风险 (参见 INT30C 保证无符号整型数操作不会出现回绕)。一般来说, 原子整数类型的有符号整型溢出行为也应该被检查和预防。加法加法是在两个算术类型的操作数之间或一个指向对象的指针和一个整数类型之间执行的 。本规则只应用于两个算术类型相加的情况(指针加法的规则参见 ARR37C. Do not add or subtract an integer to a pointer to a nonarray object 和 ARR30C. Do not form or use outofbounds pointers or array subscr ipts)。此外, 自增运算等同于加 1 运算。不合规代码在以下的不合规代码示例中, 有符号的算子 si_a 和 si_b 相加时可能会导致有符号整型溢出:void func(signed int si_a, signed int si_b) {signed int sum = si_a + si_b;/* ... */ } 合规代码这种方案保证了加法操作不可能溢出, 而无需考虑底层表示:#include void f(signed int si_a, signed int si_b) {signed int sum;if (((si_b > 0) && (si_a > (INT_MAX ‐ si_b))) || ((si_b < 0) && (si_a < (INT_MIN ‐ si_b)))) { /* Handle error */ } else {sum = si_a + si_b; }/* ... */ } 减法减法是在两个算术类型的操作数之间, 两个指向兼容对象类型的限定或未限定版本的指针之间, 或一个指向一种对象类型的指针与一个整数类型的值之间执行的 。本规则只应用于两个算术类型的操作数之间的减法(关于指针运算参见 ARR36C. Do not subtract or compare two pointers that do not refer to the same arr ay,ARR37C. Do not add or subtract an integer to a pointer to a nonarray object 和 ARR 30C. Do not form or use outofbounds pointers or array subscripts)。此外, 自减运算等同于减 1 运算。不合规代码 在以下的不合规代码示例中, 无符号的算子ui_a 和 ui_b 相减时可能会产生无符号整数回绕 。如果回绕行为是非预期的, 那么就可能导致易受攻击的安全漏洞。void func(signed int si_a, signed int si_b) {signed int diff = si_a - si_b;/* ... */ } 合规代码这种方案对参与相减的操作数进行了前置条件测试, 以保证不会出现回绕。#include void func(signed int si_a, signed int si_b) {signed int diff;if ((si_b > 0 && si_a < INT_MIN + si_b) || (si_b < 0 && si_a > INT_MAX + si_b)) { /* Handle error */ } else {diff = si_a _ si_b; }/* ... */ } 乘法乘法是在两个算术类型的操作数之间执行的。不合规代码 致有符号整型溢出:void func(signed int si_a, signed int si_b) {signed int result = si_a * si_b;/* ... */ } 合规代码两个操作数的乘积可以总是使用比两个操作数中的较大者的位数多两倍的位来表示 。 以下的合规方案在 long long 类型长度至少为 int 类型长度的两倍的系统上, 消除了有符号整型溢出的情况:#include #include #include #include extern size_t popcount(uintmax_t);#define PRECISION(umax_value) popcount(umax_value)void func(signed int si_a, signed int si_b) {signed int result;signed long long tmp;assert(PRECISION(ULLONG_MAX) >= 2 * PRECISION(UINT_MAX));tmp = (signed long long)si_a * (signed long long)si_b; /* * If the product cannot be represented as a 32 _bit integer, * handle as an error condition. */if ((tmp > INT_MAX) || (tmp < INT_MIN)) {/* Handle error */ } else {result = (int)tmp; }/* ... */ } 如果 long long 的长度小于 int 的精度长度的两倍, 则断言会失败 。其中,宏 PRECISION() 和函数 popcount() 能够计算任何整数类型的正确精度 (参见 INT35C 使用正确的整数精度)。合规代码以下的可移植的合规解决方案能够应用于任何符合标准的编译器, 即使这些编译器并不存在任何精度为 int 类型两倍的整数类型:#include void func(signed int si_a, signed int si_b) { signed int result;if (si_a > 0) {/* si_a is positive */if (si_b > 0) { /* si_a and si_b are positive */ if (si_a > (INT_MAX / si_b)) {/* Handle error */ } } else { /* si_a positive, si_b nonpositive */ if (si_b < (INT_MIN / si_a)) { /* Handle error */ } } /* si_a positive, si_b nonpositive */ } else { /* si_a is nonpositive */ if (si_b > 0) { /* si_a is nonpositive, si_b is positive */ if (si_a < (INT_MIN / si_b)) {/* Handle error */ } } else { /* si_a and si_b are nonpositive */ if ( (si_a != 0) && (si_b < (INT_MAX / si_a))) {/* Handle error */ } } /* End if si_a and si_b are nonpositive */ } /* End if si_a is nonpositive */ result = si_a * si_b; }除法除法是在两个算术类型之间执行的 。在补码表示的有符号整数除法中,当有符号整数类型的最小值(负值) 除以 1 时, 就会发生溢出 。 除法运算还可能导致除零错误(参见INT33C 保证除法和取余运算不会导致除零错误)。不合规代码以下的不合规代码防止了除零错误(INT33C 保证除法和取余运算不会导致除零错误), 但是却没有防止补码表示下的有符号整型溢出错误。void func(signed long s_a, signed long s_b) {signed long result;if (s_b == 0) {/* Handle error */ } else {result = s_a / s_b; }/* ... */ } 在 x8632 架构下, 溢出产生的错误很容易导致拒绝服务攻击。 合规代码以下的合规解决方案消除了除零错误和有符号整型溢出的可能性: void func(signed long s_a, signed long s_b) { signed long result;if ((s_b == 0) || ((s_a == LONG_MIN) && (s_b == ‐ 1))) {/* Handle error */ } else {result = s_a / s_b; }/* ... */ } 取余取余运算符用于计算两个整型值相除时的余数 。 由于很多平台都使用相同的指令实现取余和除法运算, 因此取余操作也同样容易导致算术溢出和除零错误(参见 INT33C 保证除法和取余运算不会出现除零错误)。不合规代码以下的不合规代码防止了除零错误(INT33C 保证除法和取余运算不会导致除零错误), 但是却没有防止补码表示下的有符号整型溢出错误。void func(signed long s_a, signed long s_b) {signed long result;if (s_b == 0) {/* Handle error */ } else { result = s_a % s_b; }/* ... */ } 在 x8632 架构下, 有符号整型的取余操作符是使用 idiv 指令代码与除法操 作 符 一 起 实 现 的 。 既 然 LONG_MIN / ‐ 1 会 溢 出 并 导 致 软 件 异常, LONG_MIN % _1 也同样如此。合规代码以下的合规解决方案对取余运算的操作数进行了测试, 保证不会出现溢出:void func(signed long s_a, signed long s_b) {signed long result;if ((s_b == 0) || ((s_a == LONG_MIN) && (s_b == ‐ 1))) {/* Handle error */ } else {result = s_a % s_b; }/* ... */ } 左移位操作符左移位运算是在两个整数操作数之间执行的 。E1 << E2 的结果是 E1 向左移 动 E2 个 bit 位 , 其 中 空 位 填 充 零 。 C 标 准 6.5.7 第 4 段(ISO/IEC 9899:2011) 表示:如果 E1 是一个有符号类型的非负值, 并且 E1 × 2E2 能够被结果类型所表示, 则就以其作为结果值; 否则, 该行为是未定义的。 在几乎所有情况下, 试图移动负数个 bit 位或者移动超出操作数位数的 bit 位数, 都会导致逻辑错误 。这一点在 INT34C 不要将表达式移动负数位或移动大于等于操作数中存在的位数中进行说明。不合规代码以下的不合规代码进行左移操作时, 对移位数非负, 以及移位的数目为有效值的要求进行了验证 。其中, 宏 PRECISION() 和 函数 popcount() 能够计算任何整数类型的正确精度(参见 INT35C 使用正确的整数精度)。但是, 由于代码中未进行溢出检查, 因此结果仍然有可能得到一个无法表示的值。#include #include #include extern size_t popcount(uintmax_t);#define PRECISION(umax_value) popcount(umax_value)void func(signed long si_a, signed long si_b) {signed long result;if ((si_a < 0) || (si_b < 0) ||(si_b >= PRECISION(ULONG_MAX)) {/* Handle error */ } else {result = si_a << si_b; }/* ... */ } 合规代码以下的合规解决方案消除了左移位运算中溢出的可能:#include #include #include extern size_t popcount(uintmax_t);#define PRECISION(umax_value) popcount(umax_value)void func(signed long si_a, signed long si_b) {signed long result;if ((si_a < 0) || (si_b < 0) ||(si_b >= PRECISION(ULONG_MAX) || (si_a > (LONG_MAX >> si_b))) { /* Handle error */ } else {result = si_a << si_b; }/* ... */ } 3.4.4.4【强制】保证除法和取余运算不会导致除零错误 C 标准认为在以下条件下进行除法或取余运算将会导致未定义行为: UB Description ## 45 / 或 % 运 算 符 的 第 二 个 操 作 数 为 零(6.5.5) 因此, 必须保证除法和取余运算不会导致除零错误。 #### 3.4.5 C/C++数组和指针 3.4.5.1【强制】外部数据作为数组索引时必须确保在数组大小范围内 外部数据作为数组索引对内存进行访问时, 必须对数据的大小进行严格的校验, 否则为导致严重的错误 。 下面的代码, 通过 if 语句判断 offset 的合法性:int Foo(BYTE *buffer, int size) {...int offset = ReadIntFromMsg(); if (offset >= 0 && offset < size) { BYTE c = buffer[offset];... } ... } 3.4.5.2【强制】 指针变量 、表示资源描述符的变量 、BOOL 变量声明必须赋予初值变量声明赋予初值, 可以避免由于编程人员的疏忽导致的变量未初始化引用。示例:SOCKET s = INVALID_SOCKET;unsigned char *msg = NULL;BOOL success = FALSE;int fd = -1;以下代码, 由于变量声明未赋予初值, 在最后 free 的时候出错。 char *message; // 错误! 必须声明为 char *message = NULL;...if (condition) {message = (char *)malloc(len); ... } ...if (message != NULL) {free(message); //如果 condition 未满足, 会造成 free 未初始化的内存。 }例外 1: 对全局变量, 静态变量, 在编译阶段自动初始化为 0 或者等于NULL, 不用在定义时强制初始化 。 例如:OS_SEC_BSS TICK_ENTRY_FUNC g_pfnTickTaskEntry;OS_SEC_BSS volatile UINT64 g_ullSleepTime;OS_SEC_BSS volatile UINT64 g_ullSleepBegin;OS_SEC_BSS volatile UINT64 g_ullSleepEnd; 3.4.5.3【强制】严禁对指针变量进行 sizeof 操作 编码人员往往由于粗心, 将指针当做数组进行 sizeof 操作, 导致实际的执行结果与预期不符 。 下面的代码, buffer 和 path 分别是指针和数组, 编码人员想对这 2 个内存进行清 0 操作, 但由于编码人员的疏忽, 第 5 行代码, 将内存大小误写成了 sizeof, 与预期不符。char *buffer = (char *)malloc(size);char path[MAX_PATH] = {0};...memset(path, 0, sizeof(path));memset(buffer, 0, sizeof(buffer));如果要判断当前的指针类型大小, 请使用 sizeof(char *)的方式。实际申请地址的大小应为: size * sizeof(char *) 3.4.5.4【建议】 同一个函数内, 局部变量所占用的空间不要过大 程序在运行期间, 函数内的局部变量保存在栈中, 栈的大小是有限的。如果申请过大的静态数组, 可能导致出现运行出错 。 建议在申请静态数组的 时候, 大小不超过 0x1000 。 下面的代码, buff 申请过大, 导致栈空间不够,程序发生 stackoverflow 异常。#define MAX_BUFF 0x1000000 int Foo() { char buff[MAX_BUFF] = {0};... } #### 3.4.6 C/C++字符和字符串 3.4.6.1【强制】不要试图修改字符串常量 字符串常量是双引号内的 0 个或多个字节字符的序列 (如 ”xyz ” ),宽字符串常量基本相同, 区别在于它有字母前缀‘L ’(例如 L ”xyz ” )。在编译时, 字符串常量用于创建一个长度足以容纳字符序列和 null 终止符的具有静态持久期的数组 。标准并没有指定这些数组是否不同 。如果一个程序试图修改字符串常量, 其行为是未定义的, 但是常常会导致访问违规,因为字符串常量一般存储在只读内存中。不要试图修改字符串常量, 可以使用命名的字符数组实现可修改的字符串 。不要将字符串常量(强制转换) 赋值给 non _const 指针。不合规代码在这个不合规代码示例中, char 指针 p 初始化为指向一个字符串常量的地址 。试图修改这个字符串常量会导致未定义的行为。char *p = “string literals ”;p[0] = ‘S ’;合规代码当字符串常量做为数组初始化值时, 它指定了数组中字符的初始值以及数组的长度 。这段代码在字符数组 a 分配的空间中创建了这个字符串常量的一份拷贝 。存储在 a 中的字符串可以安全地修改。char a[] = “string literals ”; a[0] = ‘S ’;不合规代码在 这 个 不 合 规 代 码 示 例 中, mktemp 函 数 入 参 是 non _const 指 针, mktemp 函数修改了它的字符串参数。mktemp(“/temp/edxxxxxxxx ”);合规代码不传递字符串常量, 而是使用一个命名数组: static char fname[] = “/temp/edxxxxxxxx ”;mktemp(fname);不合规代码在这个不合规代码示例中, 使用 strrchr 函数的 char * 类型返回结果, 修改 pathname 。 因为strrchr 的入参是一个字符串常量, 因此这种修改行为是未定义的。#include #include const char *get_dirname(const char *pathname) { char *slash;slash = strrchr(pathname, ‘/ ’); if (slash) {*slash = ‘\0 ’; /* Undefined behavior */ }return pathname; }int main(void) {puts(get_dirname( FILE ));return 0; }不合规代码 避免修改 const 对象, 即使 strrchr 函数返回的是 non _const char * 指针。仅仅将 pathname入参的类型修改为 char * 是不够的, 因为并没有强制要求编译器对将字符串常量赋值给char * 进行检查。#include #include const char *get_dirname(const char *pathname, char *dirname, size_t size) {const char *slash;slash = strrchr(pathname, ‘/ ’); if (slash) {ptrdiff_t slash_idx = slash – pathname; if ((size_t)slash_idx < size) {memcpy(dirname, pathname, slash_idx);dirname[slash_idx] = ‘\0 ’;return dirname; } } return 0; }int main(void) {char dirname[260];if (get_dirname( FILE , dirname, sizeof(dirname))) {puts(dirname); } return 0; } 3.4.6.2【建议】需要确保有足够的存储空间 部分字符串处理函数由于设计时安全考虑不足, 或者存在一些隐含的目的缓冲区长度要求, 容易被误用, 导致缓冲区写溢出 。典型函数如 itoa, realpath 。 以下的代码, 试图将数字转为字符串, 但是目标存储空间的长度不足。int num = ...char str[8] = {0};itoa(num, str, 10); // 10 进制整数的最大存储长度是 12 个字节以下的代码, 试图将路径标准化, 但是目标存储空间的长度不足。char resolvedPath[100] = {0};realpath(path, resolvedPath); //realpath 函数的存储缓冲区长度是由 PATH_MAX常量定义, 或是由_PC_PATH_MAX 系统值配置的, 通常都大于 100 字节以下的代码, 在对外部数据进行解析并将内容保存到 name 中, 考虑了name 的大小, 是正确的做法。char *msg = GetMsg();...char name[MAX_NAME] = {0};int i=0; //必须考虑 msg 不包含预期的字符 ’\n ’ while (*msg != '\0' && *msg != '\n' && i < sizeof(name) - 1) { name[i++] = *msg++; } name[i] = '\0'; //保证最后有 ’\0 ’ 3.4.6.3【强制】对字符串进行存储操作, 确保字符串有 ’\0 ’结束符 对字符串进行存储操作, 必须确保字符串有 ’\0 ’结束符, 否则在后续的调用 strlen 等操作中, 可能会导致内存越界访问漏洞。 3.4.6.4【强制】不要将非 ’\0 ’结尾的字符序列当做字符串传递给库函数 很多库函数接受一个字符串参数, 且要求该字符串是以 ‘\0 ’字符结尾的 。如果传递一个非 ‘\0 ’结尾的字符序列, 则可能导致库函数访问边界外的非法内存 。 因此, 不要传递一个非 ‘\0 ’结尾的字符序列给库函数。不合规代码下面的代码示例中, 字符序列 c_str没有以 ‘\0 ’字符结尾, 且当做入参传递给了 printf() 函数。#include void func(void) { char c_str[3] = "abc";printf("%s\n", c_str); }合规代码该解决方案在声明字符数组时未指定范围, 此时, 编译器会根据字符字面值的长度分配合适的存储空间, 用于容纳该字符字面值以及 ‘\0 ’字符。#include void func(void) { char c_str[] = "abc";printf("%s\n", c_str); }同样的, realloc 和 strncpy 使用时也需要注意置 NULL。 3.4.6.5【强制】在转换为更大的整数长度时需要把字符转换为无符号类型 有符号字符数据在赋值或转换为更大的有符号类型之前必须转换为无符号类型 。 由于编译器可以选择把 char 定义为与 signed char 相同的范围 、表示形式和行为, 因此这个规则应该同时适用于signed char 和普通的char 字符类型。 这个规则只适用于字符数据可能包含被解释为负值的字符的情况下 。 例如, 如果char 类型用 8 位补码表示, 大于 +127 的所有字符都解释为负值。不合规代码这个不合规代码示例取自 bash 1.14.6 及更早版本中的一个潜在风险, 它导致了 CERT AdvisoryCA199622 的发布 。 这个潜在风险来自 bash 源代码parse.y 模块中的 yy_string_get() 函数中的 string 指针所引用的字符数据的符号扩展。static int yy_string_get(void) {register char *c_str;register int c;c_str = bash_input.location.string;c = EOF;/* If the string doesn't exist or is empty, EOF found */if (c_str && *c_str) {c = *c_str++;bash_input.location.string = c_str; }return (c); }c_str 变量用于遍历包含了需要解析的命令行的字符串 。从这个指针提取字 符 时, 它 们 存 储 在 一 个 int 类 型 的 变 量 中 。 对 于 char 类 型 默 认 为signed char 的编译器, 这个值赋值给 int 变量时会进行符号扩展 。 以字符255(1 的补码形式) 为例, 有符号扩展导致把 1 这个值赋值给了这个整数c, 这样就无法与 EOF 区分。这个问题可以通过显式地把 c_str 变量声明为 unsigned char 类型来修正 。但是, 这个解决方案违反了“使用普通char 类型表示基本字符集中的字符 ”规范。合规代码 在这个合规解决方案中, 表达式 *c_str++ 的结果在复制给 int 变量 c 之前转换为 unsigned char 类型。static int yy_string_get(void) {register char *c_str;register int c;c_str = bash_input.location.string;c = EOF;/* If the string doesn't exist or is empty, EOF found */if (c_str && *c_str) {c = (unsigned char)*c_str++;bash_input.location.string = c_str; } return (c); } #### 3.4.7 C/C++内存管理 3.4.7.1【强制】必须释放不再需要的动态分配的内存 当通过调用标准内存分配函数分配的内存不再需要时, 必须配对调用free() 函数释放。不合规代码在这个不合规代码示例中, 通过 malloc() 分配的内存, 在指向它的指针text_buffer 生命周期结束前, 没有释放该内存。#include enum { BUFFER_SIZE = 32 };int f(void) {char *text_buffer = (char *)malloc(BUFFER_SIZE); if (text_buffer == NULL) {return __1; } // free(text_buffer); return 0; }合规代码调用 free() 函数释放内存。#include enum { BUFFER_SIZE = 32 };int f(void) {char *text_buffer = (char *)malloc(BUFFER_SIZE);if (text_buffer == NULL) {return __1; } free(text_buffer);return 0; }例外当使用具有全程序生命周期的静态指针指向分配的内存时, 可以不需要有配对的释放操作 。下面这个例子中, 通过 malloc() 分配的内存地址被赋予了 static 变量。#include enum { BUFFER_SIZE = 32 }; int f(void) {static char *text_buffer = NULL;if (text_buffer == NULL) { text_buffer = (char *)malloc(BUFFER_SIZE);if (text_buffer == NULL) {return __1; } } return 0; }3.4.7.2【强制】只释放动态分配的内存 释放并非动态分配的内存会导致严重的错误 。这个错误造成的特定后果取决于编译器, 有可能什么也不发生, 也可能导致程序异常终止 。不管是什么编译器, 应该避免对不是由动态内存分配函数所返回的指针调用free() 。向 realloc()提供一个指向并非动态分配的内存的指针也会产生类似的情况。 realloc() 函数用于改变一块动态内存的大小 。如果向 realloc() 提供一个指向并非由 malloc()这样的内存分配函数分配的内存的指针, 程序可能会异常终止。C 语言标准指出, 给 free() 或 realloc() 函数传递一个并非有内存管理函数返回的指针, 或释放已经由 free() 或 realloc() 释放的内存, 其行为是未定义的。传递空指针是安全的, 因为 C 语言规范规定给 free() 函数传递空指针, 不会有任何动作发生。不合规代码这个不合规代码示例根据 argc 的值, 把 c_str设置为一块动态分配的内存的引用或者一个静态分配的字符串常量 。不论哪种情况, c_str都做为参数被传递给 free() 。如果 c_str 所引用的并不是动态分配的内存, 对 free(c_str) 的调用就会出现错误。#include #include #include enum { MAX_ALLOCATION = 1000 }; int main(int argc, const char *argv[]) { char *c_str = NULL;size_t len;if (argc == 2) {len = strlen(argv[1]) + 1;if (len > MAX_ALLOCATION) {/* Handle error */ } c_str = (char *)malloc(len); if (c_str == NULL) {/* Handle error */ } strcpy(c_str, argv[1]); } else {c_str = "usage: $>a.exe [string]";printf("%s\n", c_str); }free(c_str);return 0; }合规代码在这个合规解决方案中, 消除了 c_str执行非动态分配内存的可能。#include #include #include enum { MAX_ALLOCATION = 1000 }; int main(int argc, const char *argv[]) { char *c_str = NULL;size_t len; if (argc == 2) {len = strlen(argv[1]) + 1;if (len > MAX_ALLOCATION) {/* Handle error */ } c_str = (char *)malloc(len); if (c_str == NULL) {/* Handle error */ } strcpy(c_str, argv[1]); } else {printf("usage: $>a.exe [string]" );return EXIT_FAILURE; }free(c_str);return 0; }3.4.7.3【强制】必须为对象分配足够的内存 作为长度参数传递给 malloc() 、 calloc() 或 realloc() 的整数值必须是合法的,足以容纳被存储的对象 。如果长度参数不正确或者可能被攻击者所操纵, 就有可能发生缓冲区溢出 。不正确的长度参数 、不充分的范围检查 、整数溢出或截断会导致分配长度不足的缓冲区 。程序员必须保证内存分配函数的长度参数能够分配足够数量的内存。不合规代码(长度计算)在这个不合规代码示例中, 分配了一个 long 类型的数组并赋值给 p 。但是, 它 使 用 了 sizeof(int)来 确 定 分 配 内 存 的 长 度 。 如 果 sizeof(long)大 于sizeof(int) , 会导致实际分配的内存不足。#include #include void function(size_t len) {long *p;if (len == 0 || len > SIZE_MAX / sizeof(long)) {/* Handle overflow */ } p = (long *)malloc(len * sizeof(int));if (p == NULL) {/* Handle error */ } free(p); }合规代码(长度计算)为了修正这个不合规代码示例, 使用sizeof(long)确定内存分配的长度。#include #include void function(size_t len) {long *p;if (len == 0 || len > SIZE_MAX / sizeof(long)) {/* Handle overflow */ } p = (long *)malloc(len * sizeof(long));if (p == NULL) {/* Handle error */ } free(p); } 另外, 还可以使用sizeof(*p) 正确地设置分配的长度。 #### 3.4.8 断言 (ASSERT) 断言是一种除错机制, 用于验证代码是否符合编码人员的预期 。编码人员在开发期间应该对函数的参数 、代码中间 执行结果合理地使用断言机制,确保程序的缺陷尽量在测试阶段被发现 。 断言被触发后, 说明程序出现了不应该出 现的严重错误, 程序会立即提示错误, 并终止执行 。 断言必须用宏进行定义, 只在调试版本有效, 终发布版本不 允许出现 assert 函数, 例如:#include #ifdef DEBUG#define ASSERT(f) assert(f) #else#define ASSERT(f) ((void)0) #endif下面的函数 VerifyUser, 上层调用者会保证传进来的参数是合法的字符串,不可能出现传递非法参数的情况 。 因 此, 在该函数的开头, 加上 4 个 ASSERT 进行校验:BOOL VerifyUser(const char *userName, const char *password) {ASSERT(userName != NULL);ASSERT(strlen(userName) > 0);ASSERT(password != NULL);ASSERT(strlen(password) > 0);... } 以下代码, SendMsg 是 CMsg 类的成员函数, socketID 是成员变量, 在调用 SendMsg 的时候必须保证 socketID 已经被初始化, 因此在此处用 ASSERT判断 socketID 的合法性。CMsg : : CMsg() {socketID = INVALID_SOCKET; }int CMsg : : SendMsg( const char *msg, int len ) {ASSERT( socketID != INVALID_SOCKET );...ret = send( socketID, msg, len, 0 );... } 3.4.8.1【强制】 断言只能在调试版使用, 必须使用宏定义, 禁止直接调用系统提供的 assert()断言只能在调试版使用, 断言被触发后, 程序会立即退出, 因此严禁在正式发布版本使用断言, 请通过编译选项进 行控制 。 错误用法如:int Foo(int *array, int size) {assert(array != NULL);... } 3.4.8.2【强制】运行时可能会导致的错误, 严禁使用断言 断言不能用于校验程序在运行期间可能导致的错误 。 以下代码的所有ASSERT 的用法是错误的。FILE *fp = fopen(path, "r"); ASSERT(fp != NULL); //文件有可能打开失败char *str = (char *)malloc(MAX_LINE);ASSERT(str != NULL); //内存有可能分配失败ReadLine(fp, str);char *p = strstr(str, 'age=');ASSERT(p != NULL); //文件中不一定存在该字符串int age = atoi(p+4);ASSERT(age > 0); //文件内容不一定符合预期 3.4.8.3【强制】严禁在断言内改变运行环境 在程序正式发布阶段, 断言不会被编译进去, 为了确保调试版和正式版的功能一致性, 严禁在断言中使用任何赋 值 、修改变量 、资源操作 、 内存申请等操作 。 例如, 以下的断言方式是错误的:ASSERT(p1 = p2); //p1 被修改ASSERT(i++ > 1000); //i 被修改 ASSERT(close(fd) == 0);//fd 被关闭3.4.8.4【建议】不要将多条语句放在同一个断言 为了更加准确地发现错误的位置, 每一条断言只校验一个条件 。 下面的断言同时校验多个条件, 在断言触发的时 候, 无法判断到底是哪一个条件导致的错误:int Foo(int *array, int size) {ASSERT(array != NULL && size > 0 && size < MAX_SIZE);... } 应该将每个条件分开:int Foo(int *array, int size) { ASSERT(array != NULL);ASSERT(size > 0);ASSERT(size < MAX_SIZE);... } #### 3.4.9 C++线程安全 说明在多线程的环境, 需要考虑并发 、线程同步 、互斥锁等相关概念, 任何操作都需要考虑有其他线程来竞争你当前拥有的资源。3.4.9.1【强制】需要确保单例中的线程安全 实现方式在某些应用环境下面, 一个类只允许有一个实例, 这就是著名的单例模式 。单例模式分为懒汉模式, 跟饿汉模式两种。首先给出饿汉模式的实现template class singleton {protected:singleton(){};private:singleton(const singleton&){};//禁止拷贝singleton& operator=(const singleton&){};//禁止赋值static T* m_instance;public:static T* GetInstance(); };template T* singleton::GetInstance() {return m_instance; }template T* singleton::m_instance = new T();在实例化 m_instance 变量时, 直接调用类的构造函数 。顾名思义, 在还未使用变量时, 已经对 m_instance 进行赋值, 就像很饥饿的感觉 。这种模式,在多线程环境下肯定是线程安全的, 因为不存在多线程实例化的问题。下面来看懒汉模式template class singleton {protected:singleton(){};private:singleton(const singleton&){};singleton& operator=(const singleton&){};static T* m_instance;public:static T* GetInstance(); };template T* singleton::GetInstance() {if( m_instance == NULL) {m_instance = new T(); }return m_instance; } template T* singleton::m_instance = NULL; 3.4.9.2【强制】 全局变量的访问如果涉及多个线程, 需要考虑多线程竞争条件问题应该尽可能减少全局变量的使用, 如果多个线程会访问到该全局变量,则访问过程必须加锁 。 以下代码中, g_list 是全局变量, 对链表进行搜索操作时, 在 while 循环语句的前后加锁。ItemList *g_list = NULL;ItemList *SearchList(const char *name) {Lock();ItemList *p = g_list;while (p != NULL) {if (strcmp(p->name, name) == 0) { break; } p = p->next; }UnLock();return p; } 性能敏感的代码, 请考虑采用原子操作或者无锁算法。 #### 3.4.10 C++ 类 3.4.10.1【强制】定义为基类的类的析构函数必须定义为虚函数 说明Class A{}; Class B : public A{};A *a = new B();定义类 A 的析构函数如果不是虚函数, 那么在通过 delete a 释放存储空间时, 派生类的对象的存储空间不会被释放(派生类的析构函数不会被调用)。实现方式定义为基类的类的析构函数必须定义为虚函数, 相反, 如果不需要基类对派生类及对象进行操作, 则不能定义虚函数, 因为这样会增加内存开销。 #### 3.4.11 C++ 函数 3.4.11.1【强制】对不安全 C++库函数进行重写后再调用 说明C++中的字符串相关库函数均为不安全函数, 当相关参数可以被外界控制 时就会存在溢出漏洞。实现方式extern char *strcpy(char *dest,char *src) char *strncpy(char *dest, char *src,size_tn) void*memcpy(void*dest,const void *src, size_t n) 把从 src 地址开始且含有 NULL 结束符的字符串赋值到以 dest 开始的地址空间,返 回 dest (地 址 中 存 储 的 为 复 制 后 的 新值)。把从 s_r_c_地址开始且含有 N_U_L_L_结束符的字符串赋值到以 d_e_s_t_开始的地址空间, 返回 d_e_s_t_ (地址中存储的为复制后的新值)。由 s_r_c_指向地址为起始地址的连续 n_个字节的数据复制到以 d_e_s_t_i_n_指向地址为起始地址的空间内 。 int printf(const char *format, ...)int fprintf(FILE *stream, const char *format, ...)int sprintf(char *str, char * format [, argument, ...])int vfprintf(FILE *stream, char *format, va_list param) int vsprintf(char *string, char *format, va_list param)scanf("<格式化字符串>", <地址表>)int sscanf( string str, string fmt, mixed var1, mixed var2 ... )extern char *strcat(char *dest,char*src) 重写这些函数的核心就是在函数内部检查输入参数的合法性, 例如源字符串长度不超过目的字符串长度 、最后一个\0 的特殊处理等等。 ### 3.5 Golang 安全开发规范 3.5.1【强制】对外部输入参数必须按类型进行数据校验 说明所有外部输入的参数, 应使用 validator 进行白名单校验, 校验内容包括但不限于数据长度 、数据范围 、数据类型与格式, 校验不通过的应当拒绝。正确示例(参数化查询):import ( "fmt" "github.com/go-playground/validator/v10" ) var validate *validator.Validate func validateVariable() {myEmail := "[邮箱已脱敏]"errs := validate.Var(myEmail, "required,email")if errs != nil {fmt.Println(errs)return //停止执行 } // 验证通过, 继续执行 ... } func main() {validate = validator.New() validateVariable() }3.5.2【强制】SQL 语句应使用预编译并绑定变量 说明应 使 用 database/sql 的 prepare 、 Query 或 使 用 GORM 等 ORM 执 行SQL 操作。正确示例:func handlerGood(db *sql.DB, req *http.Request) {q := "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='?' ORDER BY PRICE" db.Query(q, req.URL.Query()["category"]) } 反例:func handler(db *sql.DB, req *http.Request) {q := fmt.Sprintf("SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='%s' ORDER BY PRICE", req.URL.Query()["category"])db.Query(q) }3.5.3【强制】资源请求必须进行过滤验证 说明使 用 "net/http" 下 的 方 法 http.Get(url) 、 http.Post(url, contentType, body) 、 http.Head(url)、http.PostForm(url, data)、http.Do(req)时, 如变量值外部可控(指从参数中动态获取), 应对请求目标进行严格的安全校验。3.5.4【强制】模板渲染必须进行过滤验证 说明使用 text/template 或者 html/template 渲染模板时禁止将外部输入参数引入模板, 或仅允许引入白名单内字符。错误示例: func handler(w http.ResponseWriter, r *http.Request) {r.ParseForm()x := r.Form.Get("name") var tmpl = `
First name:

` + x + `

` t := template.New("main")t, _ = t.Parse(tmpl)t.Execute(w, "Hello") } 正确示例:import ( "fmt" "github.com/go-playground/validator/v10" ) var validate *validator.Validate validate = validator.New() func validateVariable(val) {errs := validate.Var(val, "gte=1,lte=100") // 限制必须是 1-100 的正整数if errs != nil {fmt.Println(errs)return false } return true } func handler(w http.ResponseWriter, r *http.Request) {r.ParseForm()x := r.Form.Get("name") if validateVariable(x) {var tmpl = `
First name:

` + x + `

` t := template.New("main")t, _ = t.Parse(tmpl)t.Execute(w, "Hello") } else { // ... } } 3.5.5【强制】跨域资源共享 CORS 必须限制请求来源 说明跨域资源共享 CORS 请求保护不当可导致敏感信息泄漏, 因此应当严格设置 Access-Control-Allow-Origin 使用同源策略进行保护。正确示例:c := cors.New(cors.Options{ AllowedOrigins: []string{"http://sageorigin.com", "https://sageorigin.com"}, AllowCredentials: true,Debug: false, }) // 引入中间件 handler = c.Handler(handler)return true } 3.5.6【建议】避免在代码中使用硬编码的 IP 地址 说明服务具有不断变化的架构, 服务 IP 地址写入硬编码是错误的 。 因为当它发生变化时, 硬编码的 IP 也必须被随之变化 。这将对产品开发 、交付和部署产生不利影响:1.开发人员须进行快速修复, 而不是让运营团队更改配置文件。2.这会导致在每个环境(开发 、测试 、生产) 中使用相同的地址。除此之外, 对应用程序安全有影响 。攻击者可能能够反编译代码, 从而发现潜在的敏感地址 。攻击者可以对服务执行 DDOS 攻击或尝试欺骗 IP 地址以绕过安全检查。正确示例:config, err := ReadConfig("properties.ini")ip := config["ip"]port := config["ip"] SocketClient(ip, port) 错误示例:var (ip = "192.168.12.42" port = 3333) SocketClient(ip, port) 例外情况:以下 IP 地址可在代码中使用, 不视为存在敏感信息:● 采用 CIDR 表示法的环回地址 127.0.0.0/8(从 127.0.0.0 到 127.255.255.255)● 广播地址 255.255.255.25● 不可路由地址 0.0.0.0i 类似这样的字符串: 2.5.., 因为它们通常与对象标识符(OID)匹配 ### 3.6 Python 安全开发规范 #### 3.6.1 数据校验 3.6.1.1【强制】禁止使用不可信数据直接拼接 SQL 语句 说明由于开发人员在拼接 SQL 时并未对参数进行处理, 导致攻击者提交的精心构造过的数据会被 SQL 解释器当作代码执行,进而引发 SQL 注入漏洞 。使用 Django 原生 API 可以很好的规避风险, 如果必须拼接 SQL 语句, 则需要采取参数化查询的方法, 并对不可信数据进行处理。正确示例(参数化查询):#!python def user_contacts(request):user = request.GET['username']sql = "SELECT * FROM user_contacts WHERE username = %s"cursor = connection.cursor()cursor.execute(sql, [user])# do something with the results result = cursor.fetchone()#or results = cursor.fetchall() cursor.close() 错误示例(直接拼接 SQL 语句进行查询):def getUsers(user_id=None):conn = psycopg2.connect("dbname='scanner' user='aurora' host='' password=''") cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)if user_id==None:str = 'select distinct * from auth_user' else:str='select distinct * from auth_user where id=%s‘ %user_id res = cur.execute(str)res = cur.fetchall()conn.close() return res 3.6.1.2【强制】禁止使用不可信数据直接拼接 XML 说明使用精心构造的 XML 数据会使得攻击者伪造的外部实体被解析, 导致XXE (XML External Entity, XML 外部实体注入攻击)。 因为 libxml2.9 以下默认解析外部实体。实现方式:禁止解析外部实体, 升级 libxml 为 2.9 或者以上, 并在使用 XMLParser的过程中设置 resolve_entities=False 。(不会读外部的东西)3.6.1.3【强制】禁止直接使用不安全的命令执行接口 说明在执行系统命令时使用了不安全的命令接口并传入了未经校验的不可信输 入 , 导 致 命 令 注 入 漏 洞 。 杜 绝 使 用 os.system 、 os.popen 、 commands.getoutput 、commands.getstatusoutput 等不安全的接 口, 并在入口进行严格的数据校验 。 使用 subprocess 模块, 并禁止使用 shell=True 参数。正确示例(使用 subprocess 模块): import shlex, subprocess command_line = raw_input() /bin/vikings -input eggs.txt -output "spam spam.txt" -cmd "echo '$MONEY'"args = shlex.split(command_line) # args 正确的分词print args ['/bin/vikings', '-input', 'eggs.txt', '-output', 'spam spam.txt', '-cmd', "echo '$MONEY'"]p = subprocess.Popen(args)错误示例(使用 os.system):#!python def myserve(request, filename, dirname):re=serve(request=request,path=filename,document_root=dirname,show_indexe s=True)filestr='authExport.dat're['Content-Disposition'] = 'attachment; filename="' + urlquote(filestr)+'"'fullname=os.path.join(dirname,filename)os.system('sudo rm -f %s'%fullname) return r 3.6.1.4【强制】禁止向前端页面输出未经处理的数据 说明如果未对用户输入进行过滤 、输出进行编码, 会导致注入恶意指令代码到网页, 使用户加载并执行攻击者恶意制造的脚本, 引发 XSS, 跨站脚本攻击 。对输入数据中的< 、> 、( 、) 、“ 、 ”、‘ 、 ’等特殊字符进行过滤, 输出时进行 html 编码, 把数据转化为 html 实体。正确示例:#!python def xss_test(request):name = request.GET['name']return render_to_response('hello.html', {'name':name}) 错误示例:#!python def xss_test(request):name = request.GET['name']return HttpResponse('hello %s' %(name)) 3.6.1.5【建议】进行重要操作时需要在请求中添加 token 说明通过在授权用户访问的页面中包含链接或者脚本的方式, 在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点, 从而在并未授权的情况下执行在权限保护之下的操作 。 为了避免 CSRF (Cross Site Request Forgery, 跨站域请求伪造), 需要在进行重要操作时在请求中添加 token 并验证。实现方法:1、验证 HTTP Referer 字段 ## 2 、在请求地址中添加 token 并验证 3、Django 中在 Settings.py 中启用 Csrfmiddleware 3.6.1.6【强制】禁止使用不可信的数据直接使用 LDAP 进行查询 说明在使用 LDAP 时, 如果未对查询的参数进行合理的过滤, 由于其自身语法的原因会导致 LDAP 注入 。LDAP 注入攻击和 SQL 注入攻击相似, 因此接下来的想法是利用用户引入的参数生成 LDAP 查询 。一个安全的 Web 应用在构造和将查询发送给服务器前应该净化用户传入的参数 。在有漏洞的环境中, 这些参数没有得到合适的过滤, 因而攻击者可以注入任意恶意代码。实现方式: ## 1 、对特殊字符进行过滤: ## 2 、使用 ldap.filter.escape_filter_chars 过滤参数 #### 3.6.2 I/O 操作 3.6.2.1【强制】进行文件相关操作时必须对 URL 进行过滤 说明文件读取时由于路径参数可控, 可穿越到上级目录, 可对任意文件删除、下载 、覆盖等, 导致任意文件读取 、下载以及目录穿越漏洞 。为了避免需要严格验证参数范围, 非法字符直接返回, 过滤“ .. ”和“ . ”字符。正确示例:import os import posixpath path = posixpath.normpath(urllib.unquote(path)) newpath = ''for part in path.split('/'):if not part:continue drive, part = os.path.splitdrive(part)head, part = os.path,split(part)if part in (os.curdir, os.pardir):continue错误示例:#!python@login_required @permission_required("accounts.newTask_assess")def exportLoginCheck(request,filename):if re.match(r“*.lic ”, filename):fullname = filename else:fullname = "/tmp/test.lic" print fullname return HttpResponse(fullname) 3.6.2.2【强制】必须对上传文件进行处理 说明文件上传时未对文件名进行过滤会导致任意类型文件上传, 如果没有对已上传文件进行重命名, 文件存储路径泄露, 会导致直接 Getshell。实现方式: ## 1 、对相关参数进行过滤 ## 2 、限定上传文件后缀 ## 3 、限定上传文件大小 ## 4 、文件上传后进行重命名 ## 5 、建立文件专用服务器 #### 3.6.3 序列化和反序列化 3.6.3.1【强制】禁止直接使用不可信数据通过 eval 执行 说明eval() 函数用来执行一个字符串表达式, 并返回表达式的值 。如果执行的命令没有进行过滤, eval()对字符串直接做处理, 会导致代码注入漏洞 。仅将 builtings 置为空, 不能保证 eval 的安全性。实现方法: ## 1 、对数据进行过滤 ## 2 、使用 ast.literal_eval()函数 针对实现方法 2, iteral_eval() 函数会判断需要计算的内容计算后是不是合法的 python 类型, 如果是则进行运算, 否则就不进行运算。3.6.3.2【强制】禁止不可信的数据直接通过 yaml 反序列化 说明在使用 yaml.load 反序列化恶意数据时, 会导致攻击者自定义的函数执行 , 从 而 导 致 代 码 注 入 漏 洞 , 使 用 安 全 函 数 yaml.safe_load() 或 者yaml.safe_load_all()替代 yaml.load()来避免 yaml 反序列化漏洞。 正确示例:# !/usr/bin/env python# -*- coding:utf-8 -*- import yaml yaml.safe_load(file('sample.yml', 'r'))错误示例:# !/usr/bin/env python# -*- coding:utf-8 -*- import yaml yaml.load(file('sample.yml', 'r')) 3.6.3.3【强制】禁止不可信的数据直接通过 pickle/Cpickle 反序列化 说明pickle/Cpickle 可以将字符串反序列化为 python 对象, 与 yaml 相似在进行 pickle.loads()/cpickler.loads()时, 如果反序列化的数据没有进行过滤, 反序列化后产生的对象会在结束时触发 reduce ()函数, 从而触发恶意代码,导致代码注入漏洞。实现方法: ## 1 、对反序列化前的数据进行严格过滤。 ## 2 、建议 json 处理。 ### 3.7 JavaScript 安全开发规范 3.7.1【强制】禁止使用 eval() 、setTimeout () 、setInterval () 函数来处理来自外部的不可信数据 说明eval() 函数存在安全隐患, 该函数可以把输入的字符串当作 JavaScript表达式执行, 容易被恶意用户利用。 同样, setTimeout ()、setInterval () 函数也可以接收一串 JavaScript 代码作为它们的第一个参数, 如果所接收的字符串是不可信的, 那么也会引起跨站脚本攻击。错误示例:将字符串直接作为 javaScript 执行, 会引起跨站脚本攻击:错误示例:setTimeoutet("alert('Hi'):",100)正确示例:setTimeout(function(){ alert("Hi"): },100) 例外情况:如果用户输入的部分作为 eval 函数参数进行拼接的一部分, 则需要对这部分输入进行 JavaScript 转义:var input = Encoder.encodeForJS(untrustedData); 3.7.2【强制】禁止直接对不可信的 JS 对象进行序列化 说明JavaScript 支持面向对象编程(OOP) 技术, 它有很多不同的内置对象,并允许用户创建对象, 一个 JS 对象可以同时拥有数据和方法, 如果这些数据或者方法来自用户输入, 那么这个对象的序列化将产生可以被注入代码利用的安全漏洞。源代码示例: 可以使用 new object() 或简单如下所示内联代码来创建新的对象。 message = {from: document.getElementById("fromEmail").value, to: document.getElementById("toEmail").value,Subject: "I am fine",Body: "Long message here", Showsubject: function() {document.write(this.subject) } }; //将 JS 对象进行序列化: var target = JSON.stringify(message);这是一个简单的消息对象, 其中有 2 个字段需要从页面输入框获得电子邮件地址, 我们可以将该对象序列化并发送到另外的页面 A 。程序员可以将它赋值到变量或者使用 eval 。如果攻击者在 from 邮件输入框中输入的是攻击的脚本, 那么访问 A 页面的用户就将成为跨站脚本攻击的受害者。解决方案:安全敏感对象不应被序列化验证反序列化对象的数据, 对特殊字符进行转义, 如引号, 尖括号, 斜杠function escape(str) {str = str.replace(/&/g, '&')str = str.replace(//g, '>')str = str.replace(/"/g, '&quto;')str = str.replace(/'/g, ''')str = str.replace(/`/g, '`')str = str.replace(/\//g, '/') return str } 3.7.3【强制】禁止直接将不可信数据组装成 JSON 对象 说明JSON 是一种简单有效的轻量级的数据交换格式, 并且包含对象 、数据 、 hash 表 、 向量和列表等数据结构 。 JSON 可用于 JavaScript 、Python 、C 、 C++ 、C#和 Prel 等语言 。在 Web2.0 应用程序上, 序列化的 JSON 是一种非常有效的交换机制 。 开发人员经常利用 Ajax 通过 JSON 方式来传递必要的信息给 DOM, 但是如果没有对用于构建 JSON 结构的来自外部的数据进行校验, 有可能发生 JSON 注入。错误示例下面是一个简单的 JSON 对象"bookmark",它使用下面的 name-value 对。 var obj = {"bookmarks":[{"Link" :"www.example.com","Desc":"Interesting link"}]}可以在链接或是 Desc 中注入恶意脚本: var obj = {"bookmarks":[{"Link" :"www.example.com","Desc":""}]} 如果它被注入到 DOM 并且执行, 它将成为 DOM 型 XSS 。这是另一种序列化恶意内容到终端用户的方法。解决方案:后端在入库前应该选择不相信任何前端数据, 将所有的字段统一进行转义处理。3.7.4【强制】生产环境中, JavaScript 代码必须经过压缩, 去除源代码里的所有不必要的字符后才能发布上线 规则说明: 压缩工具可采用 Minifier 、Uglifyjs 等, 或者推荐用 js 脚手架发布。4. 代码安全扫描要求 代码安全扫描要求, 规定了代码安全扫描的基本要求 、扫描工具 、评估方法以及应达到的代码安全基线, 并根据软件研发能力分级设置, 以实现代码安全开发规范以及基本原则的落地实施。4.1. 安全扫描基本要求 在软件开发过程中, 项目团队应遵循安全管理分册规定的软件安全基本原则和软件安全开发规范 。需要在软件开发期间 、软件上线发布前, 采用源代码安全分析工具进行代码质量和代码安全的扫描(C1), 以检验安全规范的执行情况 。安全扫描结果应基本满足软件安全开发规范制定的强制要求, 确保软件系统上线前修复强制类缺陷 、降低信息安全风险, 实现前置安全防护的目标。4.2. 安全扫描工具 源代码安全分析工具, 可识别在开发期间软件源代码的安全漏洞和质量问题并提供修复指导, 可有效帮助开发人员消除代码中的缺陷 。选取源代码安全分析工具应符合下述条件:1. 至少支持 Java, C/C++,Golang,Python,JavaScript, Jsp, SQL 等常见编码语言。2. 安全漏洞分析拥有全面的代码漏洞规则库, 并且将所有漏洞知识库对外公开发布。3. 能够支持检测主流的国际标准和规范规定的安全漏洞, 国际标准和规范包括OWASP Top 10 、OWASP Mobile 、PCI 、SANS Top25 、CWE。4.3. 安全扫描评估方法 #### 4.3.1 结果评估 安全扫描结果需对检测出的问题按风险等级划分严重程度,并提供按严重程度分类后的问题数量,用于源代码的安全等级评估 。按风险等级的从高到低, 严重程度划分为严重 、高危 、 中等 、低风险四个级别。根据问题严重程度的分布情况, 可将代码的安全等级, 从高到低依次设置为: 5 、4 、3 、2 、1.(C2) ## 5 级: 无问题; ## 4 级: 存在低风险问题, 无中等及以上风险的问题; ## 3 级: 存在中等风险问题, 无高危及以上风险的问题; ## 2 级: 存在高危风险问题, 无严重及以上风险的问题; ## 1 级: 存在严重风险问题。 安全扫描结果需提供源代码的代码总行数 、可执行代码行数, 用于评估源代码码缺陷密度 。(C2)安全扫描结果需提供满足 OWASP TOP 10 、CWE TOP 25 等分组条件的问题类型及数量, 用于评估源代码是否满足代码安全基线 。(C2) #### 4.3.2 缺陷处理 安全扫描结果的安全等级应达到 3 级及以上, 扫描结果中出现的严重级和高危级问题, 原则上应参考修复意见进行修复解决 。(C3)安全扫描结果原则上应满足本分册制定的安全开发规范 、代码安全基线,需要按照安全开发规范及代码安全基线中的要求进行修复解决 。(C3) #### 4.3.2 缺陷审计 安全扫描工具检出的严重级 、高危级以及不满足代码安全基线的缺陷原则上都需要修复, 如项目开发团队确认为误报且不存在安全隐患或已通过其他技术手段修复, 需进行代码安全审计并报备 。(C3)4.4. 代码安全基线 本规范以 OWASP TOP 10 和 CWE TOP 25 枚举的缺陷列表建立代码安全的参考基线, 即代码安全审查应满足的最低要求, 而非通过准则, 关于安全扫描的评估方法参见 4.3 节 。(C3)附录给出的 OWASP TOP 10 和 CWE TOP 25 缺陷列表仅作为参考, 软件开发团队应每年更新到最近发布的版本作为参考基线 。 由源代码安全分析工具检出的缺陷, 若属于此两类缺陷列表原则上均应修复, 关于缺陷处理的原则参见 4.3 节。 5. 软件安全部署规范 1. 【强制】开启服务器安全管控软件, iptables 在正式上线之前需要开启, 并保证 zabbix 等监控软件监控能监控到运行状态。2. 【建议】建议开启 selinux。3. 【建议】常见易被攻击的应用中间件需要配置加密, 避免无配置或默认配置运行 。包括但不限于: mysql ,redis,memcache, sftp 等4. 【建议】管控服务应用部署端口, 所有服务需要通过 iptables 配置源 IP到目的 IP 的强端口限制。5. 【建议】公网全网放开的服务需要提前向安全服务部或其他安全部门报备,评估后方可以放开。6. 【强制】严禁执行重大风险操作指令说明:在生产环境中, 实施的任何变更都有可能影响不可估量的风险或影响, 尤其要避免一些高危指令的执行, 包括不限于以下命令:1.rm -rf 2.任意命令 > /dev/sda (执行了任意命令, 并输出到/dev/sda 。导致/dev/sda里面的文件会被命令输出的内容全部替换掉, 最后丢失掉其中原有的数据 。)3.mv 指定的文件夹到 /dev/null 4.mkfs.ext3 /dev/sda 5.service iptables save (正确命令: systemctl restart iptables)6.dd if=/dev/random of=/dev/sda 7.drop database XXX 8.reboot 9.systemctl restart network 或 /etc/init.d/network restart 附录 附录 1 OWASP TOP 10 2021 缺陷列表 OWASP Top 10 是针对开发人员和 Web 应用程序安全性的标准意识文档,最近发布的版本为 OWASP Top 102021, 关于每类缺陷的详尽描述 、预防措施和攻击范例, 详见项目主页: https://owasp.org/www-project-top-ten/。 OWASP TOP 10 2021 缺陷的简要列表: 附录 2 CWE TOP 25 2021 缺陷列表 CWE Top 25 2021, 是 2021 年发布的常见弱点枚举(CWE) 排名前 25 的最危险的软件缺陷, 简称 CWE Top 25 。这些缺陷通常很容易被发现 、利用, 并且可以让对手完全接管系统 、窃取数据或阻止应用程序运行 。CWE 团队利用了美国国家标准与技术研究院 (NIST)国家漏洞数据库(NVD)中的常见漏洞和暴露(CVE®)数据以及常见漏洞评分系统 (CVSS)分数与每个 CVE 记录相关联 。对数据应用了一个公式, 以根据发生率和严重程度对每个弱点进行评分, 从而得出该缺陷列表 。有关 CWE Top 25 更详尽的说明见其主页:https://cwe.mitre.org/top25/archive/2021/2021_cwe_top25.html。CWE Top 25 缺陷的简要列表: 附录 3 引用 1. fortify 分类法: 软件安全错误 https://vulncat.fortify.com/zh-cn/weakness 2. OWASP 安全编码指南 http://www.owasp.org.cn/OWASP-CHINA/owasp-project 3. Common Weakness Enumeration (CWE) http://cwe.mitre.org/4. OWASP 主页: http://www.owasp.org.cn/OWASP-CHINA/5. OWASP TOP 10 主页: https://owasp.org/www-project-top-ten/6. CWE TOP 25 主页:https://cwe.mitre.org/top25/archive/2021/2021_cwe_top25.html 7. 《关于印发中国电信 IT 安全保障体系建设规范 v2.0 及 2012 年重点工作的通知》(中国电信[2012]760 号)8. 《关于印发操作系统等安全配置要求的通知》(中国电信〔2011〕555 号)9. 《中华人民共和国国家标准-信息安全技术-个人信息安全规范》 (GB/T35273-2017)10. 关于印发中国电信用户个人信息保护工作提升实施细则的通知》(中国电信[2013]411 号)11. 关于印发中国电信网络信息安全技术白皮书(2017)版的通知》(中国电信[2017]498 号)12. 腾讯安全编码规范:https://github.com/Tencent/secguide#%E4%BB%A3%E7%A0%81%E5%AE%89% E5%85%A8%E6%8C%87%E5%8D%97