【摘 要】 源代码是软件的根本,其存在的安全缺陷是导致软件出现漏洞的根源所在,人为因素的影响使得每个应用程序的源代码都可能存在安全漏洞。从根本意义上来说,代码审计就是挖掘源代码中存在的代码安全问题。本文对源代码安全问题进行分析,阐述了源代码审计方法和流程,通过研究Java Web漏洞产生的原因及源代码表现形式,给出了源代码安全编码原则,总结出消除相应漏洞的安全编码规范。
【关键词】 源代码安全审计 漏洞检测 编码规范 静态检测
1 引言
随着计算机信息技术的发展,应用软件呈现开放化、智能化、融合化、多元化等发展趋势。应用软件由于源代码编写不规范或引用的第三方框架、开源组件存在安全漏洞,从而导致软件中隐藏着安全漏洞或质量缺陷。这些安全漏洞或质量缺陷一旦被攻击者利用,将会威胁软件系统和其应用数据的机密性、完整性、可用性。开放式Web应用程序安全项目(Open Web Application Security Project,OWASP)2017年发布的Web应用安全十大漏洞中有8项和源代码缺陷相关。
应用软件和信息系统的安全问题必须从底层源头出发,进行源代码安全审计,检查代码的安全缺陷,编写是否遵循安全编程规范,开发是否使用了不安全的第三方组件,从而在安全事件发生前或漏洞隐患尚未被利用前有效规避大部分应用程序的代码问题,提高系统的主动安全防御能力。
2 源代码安全审计的方法和流程
2.1 源代码安全审计方法
源代码安全审计的主要目的是提高源代码质量,通过对程序源代码进行检查和分析,发现源代码在软件设计、测试、应用部署等各阶段中可能存在的安全缺陷或安全漏洞,从源头上避免潜在的安全风险。源代码审计技术可分为静态检测(Static Analysis Security Testing,SAST)、动态检测(Dynamic Analysis Security Testing,DAST)及动静结合检测(Interactive Application Security Testing,IAST)。
静态检测是指在不运行程序代码的情况下,对程序中数据流、控制流、语义等信息进行分析,配合数据流分析和污点分析等技术,对程序代码进行抽象和建模,分析程序的控制依赖、数据依赖和变量受污染状态等信息,通过安全规则检查、模式匹配等方式挖掘程序源代码中存在的漏洞。源代码漏洞静态检测方法一般是将源代码转化为单词(token)、树、图等代码中间表示形式,结合不同的检测算法和检测模型进行检测。
动态检测是指向程序输入人为构造的测试数据,根据系统功能或数据流向,对比实际输出结果与预想结果,分析程序的正确性、健壮性等性能,判断程序是否存在漏洞。动态检测技术主要分为3种:模糊测试、动态符号执行和动态污点分析。
动静结合检测是一种将静态分析和动态分析相结合的混合式漏洞检测方法,先使用静态检测方法对大规模的软件源代码进行检测,对大规模的软件源代码进行切分,有针对性地进行检测。再使用动态检测方法对已划分的程序代码进行数据输入,根据数据流向来判断漏洞是否存在。
2.2 源代码静态审计思路
源代码审计的思路有以下3个。根据敏感函数来逆向追踪参数的传递过程,即检查敏感函数的参数,然后回溯变量,判断变量是否可控且是否经过严格过滤;正向追踪变量传递过程,观察是否有变量输入到高风险函数中,或传递的过程中是否有代码逻辑漏洞;通读全文代码,根据自身的经验判断漏洞位置,直接挖掘功能点漏洞。
2.3 源代码静态审计流程
常见的静态分析工具有: CodeQL、Fortify、CoBOT、Coverity、 RIPS、FindBugs、Cppcheck等。依据分析目标的不同,静态分析可分为:面向源代码的静态分析和面向二进制代码的静态分析。面向源代码的静态分析以程序的源代码作为输入,对其进行语法分析、语义分析,并转换为某种特定形式的中间表示,基于该中间表示进行数据流分析、控制流分析等。面向二进制代码的静态分析则是以经过反汇编等手段处理后的二进制代码作为输入,运用模式匹配或补丁对比等方式实现漏洞检测,当程序是以二进制形式发布的,需要使用面向二进制代码的漏洞分析。本文主要研究面向源代码的静态检测技术的原理和流程。
源代码通过编译器进行词法分析、语法分析和语义分析,生成抽象的中间表示,这些中间表示蕴含了源代码的特定信息。例如,在语法分析阶段,语法分析器会产生蕴含丰富语法信息的抽象语法树。抽象语法树中叶子节点用于表示操作数,非叶子节点用于表示操作符,树的层次结构表示代码语句间的嵌套关系。为了更有效地捕获代码的语法、语义及上下文信息,学术界提出使用抽象语法树、控制流程图、程序依赖图等代码的中间抽象表示建立漏洞挖掘模型。源代码审计流程如图1所示。
图1 源代码审计流程
2.3.1 词法分析
词法分析阶段是编译前端的第一个阶段,这个阶段的任务就是从左到右逐个字符对构成源程序的字符串进行扫描,即对源代码进行扫描,并按照定义好的词法规则进行解析,识别出一个个token,将源代码分割成由一个个token组成的数组,即形成一个token流。
2.3.2 语法分析
语法分析是在词法分析生成的token序列基础上,识别token序列之间的关系,并表示成一种后续程序处理过程中更容易理解和访问的中间表示形式,我们也称为抽象语法树。同时,将有关源代码信息存放在符号表的数据结构中,符号表和中间表示形式一起用来构造目标程序。
2.3.3语义分析
语义分析在抽象语法树的基础上进行分析,分析时考虑被检测程序的基本语义,根据上下文环境检测源代码中是否存在语义错误。
2.3.4控制流图构建
控制流图反映了程序中各语句之间的先后执行顺序。控制流图是一个有向图G=(V,E),其中V代表节点的集合,E是有向边的集合。为了更清楚地标识一个控制流图,额外加入2个控制流节点:START和STOP,其中START节点为控制流图的入口节点,STOP节点为控制流图的出口节点。控制流图中的节点V代表程序的1条语句,有向边E表示语句的控制流走向。
2.3.5数据流图构建
数据流图可以通过分析抽象语法树直接获得,数据流图能够表示语句和变量之间的数据依赖关系。通过从控制流图中提取控制依赖关系,从数据流图中提取数据依赖关系,将2种关系融合到同一张图中,进而形成程序依赖图。
2.3.6函数调用图构建
函数调用图是对程序中函数调用关系的一种静态描述,在函数调用图中,节点表示函数,边表示函数之间的调用关系。通过函数调用图可以了解程序中的函数及函数之间的调用关系。由于面向对象程序设计的多态性,程序的函数调用图只能大致表示出实际运行时函数的调用关系。
2.3.7数据流分析
数据流分析是指用来获取有关数据如何沿着程序执行路径流动的相关信息的集合。数据流分析的对象是控制流图,通过对控制流图的遍历,从中收集变量的数值产生位置及使用情况等信息,检测数据的赋值与使用是否发生不合理现象,从而检测源代码中潜在的安全漏洞。
2.3.8污点分析
污点分析是一种信息流分析技术,通过对程序中的敏感数据进行标记,跟踪标记数据在程序中的传播,从而检测系统中存在的安全问题。污点分析被定义为三元组<Source, Sink, Sanitizer>,Source即污染源,表示程序从外界获取的不可信输入;Sink即污点汇聚点,通常为一组安全敏感函数,通过数据流分析跟踪程序中污点数据的传播;Sanitizer即无害处理,代表通过移除危害操作等手段使数据传播不再对程序的安全性产生危害。污点分析则是在不运行和不修改代码的情况下,对程序进行语句分析,运用程序变量之间的数据依赖关系,找出污点源(Sources)和污点汇聚点(Sink)之间的传播路径,从而找出程序潜在的漏洞隐患,再通过验证数据方法进行无害处理(Sanitizer)。
3 安全编码原则和规范
本文给出了源代码安全编码原则和源代码安全编码规范,分析了Java Web漏洞的代码缺陷及产生原因,给出了预防结构化查询语言(SQL)注入、跨站脚本攻击、跨站请求伪造、文件上传、命令执行等漏洞的安全编码规范。
3.1 安全编码原则
3.1.1数据输入验证原则
首先假设所有的输入是恶意的,除非能够证明输入数据是安全的,包括来自传感器、文件、用户或数据库等的输入,不仅要限制输入数据的类型、长度、格式和范围,还要验证输入数据是否包含有害信息。
3.1.2身份验证原则
为防止身份信息被窃取,用户身份验证信息在存储、传输等过程中应采取保护措施。在通信通道采取身份验证信息加密保护机制,如使用安全套接层(SSL)方式进行传输;口令的存储不应直接存储密码,而应存储密码的单向散列值,使用用户提供的密码重新计算哈希值,从而减轻字典攻击的威胁。同时限制用户的口令长度和复杂度,以及限制用户登录次数。
3.1.3 最小授权原则
如果恶意程序被注入软件中,进程被赋予的权限大小很大程度上决定了用户能够执行操作的类型。因此,软件中特定对象如进程、用户或计算机程序应被赋予最小权限,任何需要提权的操作,应尽可能保持最短的时间,一旦任务完成,应该立刻收回权限,从而限制潜在的安全风险。
3.1.4配置管理原则
配置管理功能应只有被授权的操作员和管理员才能访问,若确需远程管理的,应使用SSL或虚拟专业网(VPN)等加密通道进行远程管理。对于配置文件、注册表、数据库等关键配置数据应采取数据保护措施,保护关键配置数据的存储和访问的安全。
3.1.5会话管理原则
Cookie可能包括敏感信息,为防止攻击者查看或修改Cookie内容,使用密码对Cookie内容进行加密,从而避免数据受到未经授权的操作。尽量减少会话时长,会话时间越短,攻击者在捕获会话Cookie后,能使用Cookie来访问应用程序的时间就越少,因此可以减少重放攻击和会话劫持风险。
3.1.6组件安全原则
在使用第三方开源组件时,应对开源组件安全性进行评估,避免使用存在已知漏洞的开源组件或对安全漏洞进行修补后使用。
3.1.7内存安全原则
在编程过程中应避免内存出现缓冲区溢出、整数溢出、字符串格式化等问题。
3.2 常见漏洞分析及安全编码规范
3.2.1 SQL注入
3.2.1.1 SQL注入描述
SQL注入漏洞是程序将攻击者的输入参数拼接到了SQL语句中,从而构造、改变SQL语义对数据库进行攻击。
3.2.1.2 SQL注入安全编码规范
(1)加强用户输入验证
在数据请求提交数据库之前,使用处理函数或正则表达式匹配安全字符串的方法过滤用户输入内容中不合法字符。如果返回值属于特定的类型或有具体的格式,那么在拼接SQL语句之前就要进行校验,验证其有效性。未经过滤的用户输入参数不能进行SQL拼接或可扩展语言(XML)拼接。如图2所示,SQL语句将查询字符串常量与用户输入进行拼接来动态构建SQL查询命令。仅当id不包含单引号时,上述查询语句才会是正确的。
图2 禁止未经过滤的参数进行SQL拼接示例
(2)参数化查询
在使用JDBC或Hibernate框架时,使用预编译SQL语句的应用程序接口(API)进行参数化SQL查询。使用MyBatis框架时,使用#{}代替${}进行参数化查询,因为#{}会调用preparedStatement的set方法来赋值;而MyBatis只是将${}替换成变量的值,直接拼接到SQL语句中执行。当无法使用参数化查询,可使用创建白名单规定拼接到SQL查询语句中的数据集合,或使用正则校验限定拼接到SQL查询语句中的数据中可包含的字符集。
使用预编译语句prepared-Statement确保输入值在数据库中当作字符串、数字、日期或布尔值(boolean)等类型,而不是被作为SQL语法的一部分去执行,如图3所示。
图3 SQL语句参数化查询示例
(3)避免直接向用户显示数据库错误信息
避免直接向用户显示数据库错误,如类型错误、字段不匹配等,防止攻击者利用这些错误信息进一步判断数据库的有关信息。
(4)控制访问权限
将普通用户与系统管理员用户的权限严格区分开,坚持最小授权原则,从而最大程度减少因越权而导致的SQL注入攻击。
3.2.2 跨站脚本攻击
3.2.2.1 跨站脚本攻击描述
跨站脚本攻击即XSS攻击,是指前端和后端有交互但没有做输入输出过滤,应用程序将用户发送的不可信赖数据在未经过滤、转义,直接存入数据库或直接输出到页面,从而导致用户Cookie被劫持、构建Get和Post请求、获取用户信息、恶意的JavaScript执行、XSS蠕虫攻击等后果。
3.2.2.2 跨站脚本安全编码规范
跨站脚本攻击可分为持久型XSS、非持久型XSS和DOM型XSS3种类型。攻击者通过XSS漏洞进行劫持用户Cookie、构建Get和Post请求、获取用户信息、XSS蠕虫攻击等。
(1)加强用户输入验证
通过正则表达式限制输入数据中可接受的字符集合。如果输入数据为数字型参数,进行强制类型转换来校验数据的合法性,当输入数据为字符型,则应限制输入数据的长度。如果输入数据存在恶意字符,则拒绝请求。
(2)客户端辅助验证
输入数据的验证首先要在服务端进行,在客户端只能作为辅助手段进行输入数据的验证。
(3)编码处理
在不可信数据输出到页面之前,进行编码处理。使用addslashes()函数对字符串“’”“””“\”字符进行转义。使用htmispecialchars()函数对字符串“&”“””“<”“>”进行html编码。
(4)防止盗取Cookie
在重要的Cookie中加入HttpOnly属性,使得通过JavaScript脚本无法读取到Cookie信息,有效防止跨站脚本XSS攻击。
3.2.3 跨站请求伪造
3.2.3.1跨站请求伪造描述
跨站请求伪造(Cross Site Request Forgery,CSRF)是指攻击者通过伪装成来自受信任用户的请求,对受信任的网站执行操作的一种攻击方式。
3.2.3.2跨站请求伪造规范要求
(1)验证HTTP请求的Referer字段。Referer用于表明HTTP请求的来源地址。对每个HTTP请求验证其Referer值,如果Referer值是同一个域名下,则接受该请求;如果 Referer是其他网站,则有可能是CSRF攻击,拒绝该请求。
(2)在请求地址中添加token并验证。攻击者制造CSRF攻击所使用的用户验证信息均来自Cookie中,攻击者可以在不知道用户信息的情况下直接利用用户Cookie来通过安全验证。要抵御CSRF,可在HTTP请求中加入1个随机产生的token值,并在服务端校验token。正常访问时,客户端浏览器能够正确得到并传回这个随机数;而通过CSRF传来的欺骗性攻击中没有token或者token内容不正确,则拒绝该请求。
3.2.4 XML外部实体注入
3.2.4.1 外部实体注入描述
当开发人员配置XML解析功能允许外部实体引用时,攻击者通过发送恶意构造的XML数据对应用系统造成文件读取、命令执行、内网端口探测、命令执行、拒绝服务攻击等,从而导致XML外部实体注入。
3.2.4.2 XML外部实体注入规范要求
XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。DTD文档类型定义的作用是定义XML文档的合法构建模块,它使用一系列合法的元素来定义文档的结构,约束了XML文档的结构。DTD可以在XML文档内声明,也可以外部引用。
(1)在应用程序的所有XML解析器中禁用DTD文档类型定义,如图4所示。
图4 禁用DTD文档类型定义示例
(2)如果不可能完全禁用DTD文档类型定义,则禁用外部实体和参数实体,如图5所示。
图5 禁用外部实体和参数实体示例
3.2.5 文件上传漏洞
3.2.5.1文件上传漏洞描述
文件上传过程中,通常因为未校验上传文件后缀类型,导致用户可上传jsp等webshell文件。代码审计时重点关注上传文件类型校验及文件大小的限制。
3.2.5.2文件上传漏洞规范要求
(1)使用白名单方式限制可上传的文件类型,如图6所示,只允许上传扩展名为“gif”“jpg”“jpeg”“png”的图片文件。
图6 限制上传文件类型示例
(2)限制允许上传的文件大小,如图7所示,限制上传文件大小最大为100KB。
图7 限制上传文件大小示例
(3)限制文件上传目录的权限,对于文件上传目录设置可读、可写、不可执行权限,禁止用户上传的文件在后台执行。
3.2.6 命令执行漏洞
3.2.6.1命令执行漏洞描述
命令执行漏洞就是攻击者可以直接在应用中执行系统命令,从而获取敏感信息或者拿到shell权限。命令执行漏洞形成的原因是服务器对用户输入的命令没有进行验证和过滤,导致恶意代码被执行。
3.2.6.2命令执行漏洞规范要求
(1)尽量不使用系统执行命令,避免通过指定cmd /c或/bin/bash -c参数执行系统命令。
(2)应避免从客户端获取命令,无法避免时,应使用白名单对可执行的命令进行限制;白名单过大时,应使用正则校验限定用户输入数据中可包含的字符集。在系统命令需要接受用户输入时,未对输入参数作限制,允许任意命令输入。攻击者可以利用这个漏洞控制服务器,如图8所示。
图8 命令执行漏洞示例
(3)对用户输入参数中的“&&”“|”“;”等字符进行过滤。
3.2.7安全配置缺陷
3.2.7.1安全配置缺陷问题描述
安全配置缺陷通常是由于不安全的默认配置、不完整的临时配置、开源云存储、错误的HTTP标头配置,以及包含敏感信息的详细错误信息等问题造成的。
3.2.7.2安全配置缺陷规范要求
(1)删除不需要的配置和文件夹,删除Web目录下存在敏感信息的备份文件、测试文件、临时文件、旧版本文件等,关闭多余端口,停用不用的服务,如FTP、Telnet、SMTP、SSH等,改默认口令。
(2)对所使用的第三方组件进行安全配置,使得第三方组件不存在已知漏洞。
3.2.8 敏感信息泄露
3.2.8.1敏感信息泄露描述
当开发人员缺乏一定的安全意识,未按照安全规范进行编码时,会造成用户敏感信息泄露和应用程序信息的泄露。
3.2.8.2敏感信息泄露规范要求
(1)不在错误信息页泄露系统详细信息、会话标识符、用户账号信息、物理路径、数据库版本及路径、SQL语句等相关信息。
(2)删除注释代码,避免注释代码中存在遗留的测试账号信息、敏感接口地址以及第三方服务的敏感信息泄露。
(3)禁止使用Get方法传递敏感参数(会话标识、身份证号等),因为Get方法会将参数显示在URL中,传输过程中所有的代理及缓存服务器都可以直接获取用户数据。
(4)避免在前端代码中存放敏感信息,如Hidden字段存在管理员账号密码等。
(5)禁止带有敏感数据的Web页面缓存,可以通过函数设置强制浏览器不进行缓存,如图9所示。
图9 强制浏览器不进行缓存示例
3.2.9第三方组件安全
3.2.9.1第三方组件安全
为节约成本、提高开发效率,大量开源组件被引入企业软件开发过程中,因此避免使用存在安全隐患的组件对于保证软件安全具有重大意义。
3.2.9.2第三方组件规范要求
从官方渠道获取第三方组件,定期对第三方组件进行安全性检测,确保组件不存在已知安全漏洞,并及时升级组件版本;建立第三方组件库,统一进行维护和管理,避免个人从其他渠道获取;对第三方组件进行完整性验证,确保所使用的第三方组件未被篡改。
4 结语
随着云计算的普及、微服务等基础架构的成熟,企业开发模式也从传统的瀑布模型演变到持续集成安全防护理念开发安全运维一体化(Dev-SecOps)。DevSecOps理念将安全贯穿设计、开发、测试、运营生命周期的每个环节,使源代码安全漏洞能够得到尽早发现、尽快解决。源代码静态审计技术在理论上性能优异,自动化程度高,可挖掘层次深,但实际使用中存在漏报率和误报率偏高的缺陷。近年来,机器学习技术取得了重大突破,为静态审计技术注入了新的活力,基于深度学习的漏洞检测方法将会显著提高漏洞检测效率,减少误报率,是源代码安全审计下一步的研究方向。
(原载于《保密科学技术》杂志2023年2月刊)