可信编码要求
- 代码指标:CodeCheck静态扫描,最大函数长度、圈复杂度、最大函数嵌套深度、循环依赖、代码重复率
- 是否合理使用设计模式
- 是否符合基础编码规范
- 是否符合安全编码规范
- 编码是否考虑可靠、可用性
安全编码规范
参考资料
https://gitee.com/9199771/sec_coding/blob/master/sec_coding.md
安全编码规范的来源
首先我们要了解安全编码规范的来源有哪些:
安全编码规范的来源
- 公司层面的发布的文件:安全编码规范、产品安全红线要求
- 业界的安全编码规范:阿里巴巴的Java编程规范、OWASP TOP 10
- 标准化文件:信息安全技术国标
- 各种静态扫描的规则集:fortify/FindBugs这些静态扫描引擎的规则集
我们通过结合公司业务与业界规范,制定合适的安全检测逻辑的体系框架,将安全问题区分为不同的类别,在每个类别下再去丰富规则。虽然实际在公司的检测集中就有几百条甚至上千条检测规则,有很多实际上类别是一样的,只是区分的特别细致。而且对于安全检测体系这一块,一直是没有事实的业界标准,不同组织和企业,开源或非开源,设计的体系分类是有区别的。
安全编码规范总结
以下的分类和举例,也只是根据我的个人经验总结出来的:
数据校验:对所有的外部输入不信任
- API入参校验:数据类型校验、格式校验、长度校验、字符串白名单校验、存在性校验、前后端一致校验。。。
- 文件上传检查、解压缩检查(ZIP压缩包炸弹攻击)、文件大小数量类型、文件路径检查避免敏感路径以及绕过
- 外部命令校验&过滤
- 防止字符串拼接SQL语句(SQL注入)、Mybatis参数化查询(使用
#{}
) - 日志的CRLF注入,HTTP请求的CRLF注入、XSS攻击,对于一切客户端的外部输入一定要做好校验
- 注意对不可信数据的反序列化问题(Java对象)
- 对用户输入不做校验可能会造成Redis缓存穿透,一些不可信的输入可能会直接绕过缓存,直接将大量请求打到数据库
可靠性&可用性
- 正则表达式Redos攻击(拒绝服务)
- 存在死循环的分支(资源消耗)
- I/O中未对资源进行释放(try-with-resource)
- 多线程编程中对线程池的资源没有做限制,比如无界队列导致线程可以无限创建OOM,比如没有限制最大线程池大小
- 循环中做耗时的操作:SQL插入语句在循环中(正确应该在循环外使用批量插入语句)、在循环体中写日志。。。
- 临时文件使用完后要删除
- 一些重要的接口要做好调用频次限制,比如说发送短信验证码的接口,一分钟只能调用一次
- 一般情况下(但非强制)不要忽略函数的返回值(比如文件删除函数的返回值表示是否成功,但一些场景下,可能只是尝试一下,并不关心实际删除成功了没)
- 对于大对象的序列化要谨慎,因为可能构成性能和效率的问题
- 尽量避免在性能敏感的Java应用程序中频繁使用反射来创建Java对象
敏感信息泄露
- 代码中不能硬编码电话号码、身份证、工号、密钥等敏感信息,根据需要进行脱敏或者匿名化处理
- 日志中打印敏感异常
- 对于敏感信息的存储、传输应该以加密的形式进行,不允许明文存储和传输
- Java中比较特别的,就是建议使用char[]而不是String来存储敏感信息,char[]使用完之后需要清零
- 序列化时应注意敏感信息的泄露问题,不要序列化敏感的信息,如果需要序列化要进行加密处理
- 后端接口的response中应避免出现任何可能被利用的信息,包括但不限于口令、异常、详细报错等
通信安全
- 外部连接必须进行证书校验
- 使用安全的通信协议,比如使用HTTPS,禁止使用HTTP
- 限制容器/服务器的通信范围,开放的端口应遵循最少攻击面原则,只开放必须要使用的端口
安全算法使用
- 使用安全随机数生成方法
- 应使用安全的加密算法,SHA256/RSA(>=3072bits),不能使用不安全的加密算法,SHA1、MD5、RSA(<2048 bits),或者用base64用于加密
权限管控:更多是业务层面的,代码静态扫描不一定能够扫出来
- 后端接口要做好接口权限的控制,控制数据边界,避免出现越权问题
- 接口的权限项检测
- 多租管理需要做到租户数据隔离
- 容器/服务器文件权限应该设置合理的权限,容器/服务器的配置安全应默认以最小权限运行
安全审计
- 对于高危与敏感操作,比如数据库增删改、上传下载等需要记录审计日志,满足安全审计的要求
- 审计日志的记录信息应该完整,包好时间、访问id、类型、IP、事件描述、事件结果,以及其他辅助定位信息
异常处理
- 空指针、空对象的处理
- 数值溢出,运算过程中的数值溢出问题
- 运行时异常不用捕获(空指针、算数、数组越界等)
- 捕获异常再抛出新的异常,不能丢失原来的异常的信息
- 不能捕获异常而不进行任何处理(不想做逻辑处理也行,打印日志也可以)
多线程下编码安全
- 注意在多线程下使用一些非线程安全的集合会有数据不一致的问题,比如HashMap、HashSet
- 在高并发情况下使用HashMap可能导致死循环(https://www.jianshu.com/p/1e9cf0ac07f4)
- 虽然Vector、HashTable 内部的方法基本都是 synchronized,是线程安全的,但还是建议使用Java提供的Concurrent包中的集合类,开箱即用,是专门针对并发场景下提供的线程安全的集合。
- 关注多线程的编码问题,防止可能出现死锁的情况,比如使用suspend/resume来进行线程的暂停和恢复
- 加锁和解锁的顺序必须要保持一致,否则会造成死锁问题
常见的Web安全问题
TOPN问题
- 越权问题(严重问题)
- 敏感信息泄露(严重问题)
- SQL&TQL注入:当应用程序将用户输入的内由于用户的输入,也是SQL语句的一部分,所以攻击者可以利用这部分可以控制的内容,注入自己定义的语句,改变SQL语句执行逻辑,让数据库执行任意自己需要的指令容,拼接到SQL语句中,一起提交给数据库执行时,就会产生SQL注入威胁。防注入的核心思路:参数化查询+参数校验+转义
- XSS攻击
- 前后端校验
- 命令注入
其他问题
- 容器/服务端文件权限过大:容器服务端的文件权限应满足最小权限原则,避免被利用
- ReDos攻击(不安全的正则表达式Regex):一些不安全的正则通过构造某些特殊的字符串,会导致匹配时间很长,大量消耗服务器资源
- 证书校验
- CRLF注入
- 冗余接口和页面
- 不安全的加密算法
越权问题
根据公司红线要求,对于每一个需要授权访问的请求,必须核实用户的会话标识是否合法、用户是否被授权执行这个操作。否则会导致越权访问。越权主要分为横向越权和纵向越权两大类:
- 横向越权:恶意用户尝试访问同级权限水平的其他用户的特权资源。例如网上银行系统中普通储户可尝试访问到其他储户的账户信息。
- 纵向越权:低权限级别的用户尝试获取高权限级别用户的权限或者数据,造成高级别用户资源信息泄露。例如网上银行系统中普通储户访问仅授权给管理员的用户管理模块。
下面关于越权通过举几个例子来加以说明:
比如说,某个系统中存在多个租户,有的租户权限较大,可以管理其他租户数据的管理租户(公租),有只能管理和查看自己数据的业务租户,以及能够查看和管理某些特定租户的托管租户,不同租户拥有的权限不一样,所以需要在每一个接口中做好权限校验,不同租户调用同一个接口时,数据域是不同的,需要界定数据的范围。
对于纵向越权,常见的被利用的方式就是提权攻击,某些系统中存在一些接口没有做好权限管控,比如说一个工单系统中,存在一个创建免审批白名单的接口,应该具有管理员权限才能调用该接口,但只是在前端页面针对普通用户对管理页面进行了隐藏,并未对该接口进行权限管控,导致攻击者直接利用Postman或Burpsuite工具,直接用普通用户身份将该用户加入到白名单中,导致越权发生。
而对于横向越权,常见的问题就是某些接口没有对数据边界进行管控。比如说现在存在某个根据工单ID查询工单详情的接口
POST /get_order_detail
{
"order_id": "ASTR-20240308-xxxxxx",
"date": "2024-03-08",
"creator": "test"
}
该工单系统设计为,用户只能查看和处理自己创建的工单,不能查看别人创建的工单,但是由于接口没有对查询用户进行校验,可以通过猜测order_id的规律(ASTR + 日期 + 一串数字),得到其他用户的工单ID,从而查询到其他用户的工单数据。
另外还有容易忽略的,页面元素隐藏/置灰校验,我们不能期待通过隐藏页面以及元素,或者将某些按钮置灰来做到安全,这只能对正常使用的用户起作用,对于别有用心的攻击者来说,往往喜欢搞事情。比如说,某个展示用户数据列表的页面,只有管理员才有导出数据的按钮,普通用户是隐藏的,而后端导出的接口却没有做权限校验,这就是一个安全漏洞。
敏感信息泄露
日志打印、命令回显等地方不能直接打印敏感信息
对于敏感的个人信息,比如邮件、姓名、电话、身份证号要进行脱敏处理,通常采用加*号来进行处理,例如:
- 身份证号:440301198505034879 -> 4403014879
- 电话号码:18955234716 -> 1894716
对于异常不能直接打印全部异常的堆栈信息,防止堆栈信息泄露程序的关键信息,被黑客利用;对于敏感异常,例如Java中的一下这几类,更是不能随意打印到后台日志,如果要打印,日志文件一定要做好权限控制
java.io.FileNotFoundException 底层文件系统结构、文件名列举
java.sql.SQLException 数据库结构、用户名列举
java.net.BindException 非信任客户端可选择服务器端口时可枚举开放端口
java.util.ConcurrentModificationException 可能提供线程非安全代码的相关信息
javax.naming.InsufficientResourcesException 服务器资源不足(可能辅助DoS攻击)
java.util.MissingResourceException 资源列举
java.util.jar.JarException 底层文件系统结构
java.lang.OutOfMemoryError DoS攻击
java.security.acl.NotOwnerException 所有者枚举
前后端校验
前后端校验包含诸多方面,往往是安全测试中最喜欢提单的部分(有遇到过测试一个接口提一张问题单,一个月就产生上百张问题单的情况)
输入校验:前端和后端都要对用户输入进行校验,包括必填校验、存在性校验、合法性校验等等。
- 必填校验:某些值是一定要填的,比如注册新账号时,基本的用户信息,账号名、密码、邮箱是必填的
- 存在性校验:检查输入的某个值是否已经存在。比如说账号注册的时候,不能和已经注册的账号重复了;删除用户信息时,必须保证账号存在。这些都涉及数据的存在性校验。
- 合法性校验:数值类型(字符串/整形/浮点型/布尔型)、数值范围、字符串格式正则校验、特殊字符校验、枚举值校验(男/女/保密)中选择
- 后端的校验是最后的关卡。理论上来说所有的前端校验都是可以绕过的,但是后端是必经之路,所以一定要把好关
- 前端校验除了校验要正确外,还需要关注用户体验,更讲究前端技术的运用。要根据实际场景给予合理的提示信息,比如验证码输入错误、该用户已经被注册过、密码复杂度不够高等等,一般可采用冒泡提醒,使用淡入淡出丝滑特效等用户友好型的提示交互,尽量减少使用强盗式、警告式、吓人式的弹窗和提醒,只有涉及严重错误的场景才使用较为严重的提示交互
- 前后端校验要保持一致。比如,前端限制字符输入不能超过100,后端接口对应参数的限制也应该是100
XSS攻击
- 反射型:攻击者构造一个带有恶意代码的url链接诱导正常用户点击,服务器接收到这个url对应的请求读取出其中的参数然后没有做过滤就拼接到Html页面发送给浏览器,浏览器解析执行
- 存储型: 攻击者将带有恶意代码的内容发送给服务器(比如在论坛上发帖),服务器没有做过滤就将内容存储到数据库中,下次再请求这个页面的时候服务器从数据库中读取出相关的内容拼接到html上,浏览器收到之后解析执行
- DOM型:DOM型xss主要和前端js有关,是前端js获取到用户的输入没有进行过滤然后拼接到html中。DOM型XSS和前两者的区别主要是,前两者是服务端的漏洞,而DOM型XSS是前端javascript的漏洞
这里举一个反射性XSS的例子,例子来源于前端安全系列(一):如何防止XSS攻击?,现有一个HTML的页面
<input type="text" value="<%= getParameter("keyword") %>">
<button>搜索</button>
<div>
您搜索的关键词是:<%= getParameter("keyword") %>
</div>
当构造请求URL为http://xxx/search?keyword="><script>alert('XSS');</script>
时,服务端会解析出请求参数 keyword,得到"><script>alert('XSS');</script>
,拼接到 HTML 中返回给浏览器。形成了如下的 HTML:
<input type="text" value=""><script>alert('XSS');</script>">
<button>搜索</button>
<div>
您搜索的关键词是:"><script>alert('XSS');</script>
</div>
浏览器无法分辨出<script>alert('XSS');</script>
是恶意代码,因而将其执行。这里不仅仅 div 的内容被注入了,而且 input 的 value 属性也被注入, alert 会弹出两次。
CRLF注入
参考文章CRLF注入原理:https://www.cnblogs.com/mysticbinary/p/12560080.html
CRLF 指的是回车符(CR, ASCII 13, r, %0d) 和换行符(LF, ASCII 10, n, %0a),操作系统就是根据这个标识来进行换行的。严重程度要看CRLF注入到什么地方,如果是注入到日志中,会造成日志格式错乱(但实际上安全测试很喜欢给这个问题提单),影响开发人员对于日志的定位。但如果是注入到HTTP请求中,就需要注意了,可能会引发Cookie会话固定
和反射型XSS
的危害。
在HTTP当中HTTP的Header和Body之间就是用两个crlf进行分隔的,如果能控制HTTP消息头中的字符,注入一些恶意的换行,这样就能注入一些会话cookie和html代码,这种CRLF injection 又叫做 HTTP response Splitting,简称HRS。该漏洞可以造成Cookie会话固定
和反射型XSS
的危害,HRS漏洞存在的前提是:url当中输入的字符会影响到文件。
HRS漏洞存在的前提是:url当中输入的字符会影响到文件,比如在重定位当中可以尝试使用%0d%0a
作为crlf
(1)Cookie会话固定
假设服务端的PHP代码是这样处理
if($_COOKIE("security_level") == 1)
{
header("Location: ". $_GET['url']);
exit;
}
在COOKIE满足条件时,将url的参数取出来拼接在Location中,作为响应头发送给客户端。当我们别有用心的构造请求体为?url=http://baidu.com/xxx%0a%0dSet-Cookie: test123=123
时,经过服务端处理后,返回的响应头变为了,可以看到Cookie被注入到了响应头中
HTTP/1.1 200 OK
Date: Tue, 15 Oct 2024 03:19:32 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 160
Connection: keep-alive
Location: http://baidu.com/xxx
Set-Cookie: test123=123
(2)反射型xss
连续使用两次%0d%0a
就会造成header和body之间的分离,就可以在其中插入xss代码形成反射型xss漏洞。
还是借助前面的例子,当我们构造?url=http://baidu.com/xxx%0a%0d%0a%0d<script>alert('xss')</script>
时,返回的响应变成了,两次的%0d%0a
使得恶意的script代码部分注入了body的部分,被浏览器所执行,从而引发反射性XSS问题。
HTTP/1.1 200 OK
Date: Tue, 15 Oct 2024 03:19:32 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 160
Connection: keep-alive
Location: http://baidu.com/xxx
<script>alert('xss')</script>
本博客文章除特别声明外,均可自由转载与引用,转载请标注原文出处:http://www.yelbee.top/index.php/archives/222/