PHP代码审核的详细介绍
概述
代码审核,是对应用程序源代码进行系统性检查的工作。它的目的是为了找到并且修复应用程序在开发阶段存在的一些漏洞或者程序逻辑错误,避免程序漏洞被非法利用给企业带来不必要的风险
代码审核不是简单的检查代码,审核代码的原因是确保代码能安全的做到对信息和资源进行足够的保护,所以熟悉整个应用程序的业务流程对于控制潜在的风险是非常重要的。
审核人员可以使用类似下面的问题对开发者进行访谈,来收集应用程序信息。
应用程序中包含什么类型的敏感信息,应用程序怎么保护这些信息的?
应用程序是对内提供服务,还是对外?哪些人会使用,他们都是可信用户么?
应用程序部署在哪里?
应用程序对于企业的重要性?
最好的方式是做一个 checklist,让开发人员填写。Checklist 能比较直观的反映应用程序的信息和开发人员所做的编码安全,它应该涵盖可能存在严重漏洞的模块,例如:数据验证、身份认证、会话管理、授权、加密、错误处理、日志、安全配置、网络架构。
输入验证和输出显示
大多数漏洞的形成原因主要都是未对输入数据进行安全验证或对输出数据未经过安全处理,比较严格的数据验证方式为:对数据进行精确匹配
接受白名单的数据
拒绝黑名单的数据
对匹配黑名单的数据进行编码
在 PHP 中可由用户输入的变量列表如下:
$_SERVER
$_GET
$_POST
$_COOKIE
$_REQUEST
$_FILES
$_ENV
$_HTTP_COOKIE_VARS
$_HTTP_ENV_VARS
$_HTTP_GET_VARS
$_HTTP_POST_FILES
$_HTTP_POST_VARS
$_HTTP_SERVER_VARS
我们应该对这些输入变量进行检查
命令注入
安全威胁
命令注入攻击是通过把HTML代码输入一个输入机制(例如缺乏有效验证限制的表格域)来改变网页的动态生成的内容,而这样就可能会导致恶意命令掌控用户的电脑和他们的网络。PHP执行系统命令可以使用以下几个函数:system、exec、passthru、``、shell_exec、popen、proc_open、pcntl_exec,我们通过在全部程序文件中搜索这些函数,确定函数的参数是否会因为外部提交而改变,检查这些参数是否有经过安全处理。
代码示例
例1:
//ex1.php
<?php
$dir = $_GET["dir"];
if (isset($dir))
{
echo "<pre>";
system("ls -al".$dir);
echo "</pre>";
}
?>
我们提交
http:// localhost/ex1.php?dir=| cat /etc/passwd
提交以后,命令变成了
system("ls -al | cat /etc/passwd");
防范方法
1、尽量不要执行外部命令
2、使用自定义函数或函数库来替代外部命令的功能
3、使用escapeshellarg函数来处理命令参数
4、使用safe_mode_exec_dir指定可执行文件的路径
esacpeshellarg函数会将任何引起参数或命令结束的字符转义,单引号“'”,替换成“\'”,双引号“"”,替换成“\"”,分号“;”替换成“\;”, 用safe_mode_exec_dir指定可执行文件的路径,可以把会使用的命令提前放入此路径内。
safe_mode = On
safe_mode_exec_di r= /usr/local/php/bin/
跨站脚本威胁(Cross Site Scripting)
安全威胁
Cross Site Script(XSS),跨站脚本威胁。攻击者利用应用程序的动态展示数据功能,在 html 页面里嵌入恶意代码。当用户浏览该页之时,这些嵌入在 html 中的恶意代码会被
执行,用户浏览器被攻击者控制,从而达到攻击者的特殊目的。输出函数经常使用:echo、print、printf、vprintf、<%=$test%>
跨站脚本攻击有以下三种攻击形式:
(1) 反射型跨站脚本攻击
攻击者会通过社会工程学手段,发送一个 URL 连接给用户打开,在用户打开页面的同时,浏览器会执行页面中嵌入的恶意脚本。
(2) 存储型跨站脚本攻击
攻击者利用 web 应用程序提供的录入或修改数据功能,将数据存储到服务器或用户cookie 中,当其他用户浏览展示该数据的页面时,浏览器会执行页面中嵌入的恶意脚本。所有浏览者都会受到攻击。
(3) DOM 跨站攻击
由于 html 页面中,定义了一段 JS,根据用户的输入,显示一段 html 代码,攻击者可以在输入时,插入一段恶意脚本,最终展示时,会执行恶意脚本。DOM 跨站和以上两个跨站攻击的差别是,DOM 跨站是纯页面脚本的输出,只有规范使用 JAVASCRIPT,才可以防御。
恶意攻击者可以利用跨站脚本攻击做到:
(1) 盗取用户 cookie,伪造用户身份登录。
(2) 让浏览者被迫执行某页面操作,以用户身份向服务器发起请求,达到攻击目的。
(3) 结合浏览器漏洞,下载病毒木马到浏览者的计算机上执行。
(4) 衍生 URL 跳转漏洞。
(5) 让官方网站出现钓鱼页面。
(6) 蠕虫攻击
代码示例
直接在 html 页面展示“用户可控数据”,将直接导致跨站脚本威胁。
<?
echo “<span>$newsname</span>”;
echo “<a href=“$gifurl”>$gifname</a>”;
echo “<input type=text name=user value=\”$username\”>”;
echo “<span style=‘$stylelayout'>”. htmlentities($context).”</span>”;
?>
这几种显示方式,都可能导致用户浏览器把“用户可控数据”当成 JS/VBS 脚本执行,或页面元素被“用户可控数据”插入的页面 HTML 代码控制,从而造成攻击。
解决方案
a) 在 HTML 中显示“用户可控数据”前,应该进行 htmlescape 转义。
htmlspecialchars($outputString,ENT_QUOTES);
进行 html 转义应该按照以下列表进行转义:
& --> &
< --> <
> --> >
" --> "
' --> '
b) 在 javascript 中输出的“用户可控数据”,需要做 javascript escape 转义。
需要转义的字符包括:
/ --> \/
' --> \'
" --> \"
\ --> \\
c) 对输出到富文本中的“用户可控数据”,做富文本安全过滤(允许用户输出 HTML 的情况),防止富文本编辑器中存在脚本性的 script 代码。
SQL 注入(SQL Injection)
安全威胁
当应用程序将用户输入的内容,拼接到 SQL 语句中,一起提交给数据库执行时,就会产生 SQL 注入威胁。由于用户的输入,也是 SQL 语句的一部分,所以攻击者可以利用这部分可以控制的内容,注入自己定义的语句,改变 SQL 语句执行逻辑,让数据库执行任意自己需要的指令。通过控制部分 SQL 语句,攻击者可以查询数据库中任何自己需要的数据,利用数据库的一些特性,可以直接获取数据库服务器的系统权限。本来 SQL 注入攻击需要攻击者对 SQL 语句非常了解,所以对攻击者的技术有一定要求。但是几年前,已经出现了大量 SQL 注入利用工具,可以让任何攻击者,只要点几下鼠标,就能达到攻击效果,这使得 SQL 注入的威胁,极大增加。
SQL注入攻击的一般步骤:
1、攻击者访问有SQL注入漏洞的站点,寻找注入点
2、攻击者构造注入语句,注入语句和程序中的SQL语句结合生成新的sql语句
3、新的sql语句被提交到数据库中执行 处理
4、数据库执行了新的SQL语句,引发SQL注入攻击
代码示例
对于输入检查不充分,导致 SQL 语句将用户提交的非法数据当作语句的一部分来执行。
示例:
<?
$id=$_GET['id'];
$name=$_GET['name'];
$sql="select * from news where `id`=$id and `username`='$name' ";
?>
解决方案
a)安全配置与编码方式,PHP 配置选项在 php.ini 文件中指定。下列配置方式能够加强php 的安全性,使应用程序避免受到 sql injection 的攻击。
1) safe_mode=onPHP,将通过文件函数或其目录检查当前脚本的拥有者是否和将被操作的文件的拥有者相匹配 ,当前脚本拥有者和文件操作拥有者不匹配则为违法操作
2) magic_quotes_gpc=on / off,如果该选项被激活,那么请求参数中包含的任何单引号、双引号、反斜线和空字符都会用一个反斜线自动转义。
3) magic_quotes_sybase=on/off,如果改选项被禁用,那么 PHP 就会用一个单引号转义所有的单引号。
验证数字型的变量
$id=(int)$id;
注:PHP6 已经删除 magic quotes 选项
b)使用预处理执行 SQL 语句,对所有传入 SQL 语句中的变量,做绑定。这样,用户拼接进来的变量,无论内容是什么,都会被当做替代符号“?”所替代的值,数据库也不会
把恶意用户拼接进来的数据,当做部分 SQL 语句去解析。示例:
$stmt = mysqli_stmt_init($link);
if (mysqli_stmt_prepare($stmt, 'SELECT District FROM City WHERE Name=?'))
{
/* bind parameters for markers */
mysqli_stmt_bind_param($stmt, "s", $city);
/* execute query */
mysqli_stmt_execute($stmt);
/* bind result variables */
mysqli_stmt_bind_result($stmt, $district);
/* fetch value */
mysqli_stmt_fetch($stmt);
mysqli_stmt_close($stmt);
}
/* close connection */
mysqli_close($link);
文件上传威胁(File Upload)
安全威胁
PHP 文件上传漏洞主要在于验证文件类型的时候没处理好文件变量所带来的攻击,导致程序判断逻辑被绕过,攻击者上传脚本文件被服务器解析,从而获取 SHELL 或者上传时
文件被任意拷贝,甚至上传脚本木马到 web 服务器上,直接控制 web 服务器。
代码示例
处理用户上传文件请求的代码,这段代码没有过滤文件扩展名。
<?
// oldUpload.php
if(isset($upload) && $myfile != "none“ && check($myfile_name)) {
copy($myfile, "/var/www/upload/".$myfile_name);
echo "文件".$file_name."上传成功!点击<a href=\"$PHP_SELF\">继续上传</a>";
exit;
}
//checkUpload.php
$DeniedExtensions=array('html','htm','php','php2','php3','php4','php5','ph
tml','pwml','inc','asp','aspx','ascx','jsp','cfm','cfc','pl','bat','exe','
com','dll','vbs','js','reg','cgi','htaccess','asis') ;
if($checkUpload($_FILE[‘myfile'][name], $DeniedExtensions)){copy($_FILE[‘myfile'][tmp_name],'upload/'.$_FILE[‘myfile'][name]);
}
?>
<title>文件上传</title>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
</head>
<body bgcolor="#FFFFFF">
<form enctype="multipart/form-data" method="post">
上传文件:
<input type="file" name=“myfile" size="30">
<input type="submit" name="upload" value="上传">
</form>
</body>
</html>
解决方案
处理用户上传文件,要做以下检查:
(1) 检测文件后缀名是否符合白名单规范。
(2) 将文件按照随机文件名的形式,保存在服务器上。
(3) 上传目录脚本文件不可执行
(4) 注意%00 截断
(5) 对于 jpg 文件,需要读取文件内容,然后生成一个新的 jpg 文件进行保存
Cross-Site Request Forgery (CSRF)
安全威胁
Cross-Site Request Forgery(CSRF),伪造跨站请求。攻击者在用户浏览网页时,利用页面元素(例如 img 的 src),强迫受害者的浏览器向Web 应用程序发送一个改变用户信息的请求。由于发生 CSRF 攻击后,攻击者是强迫用户向服务器发送请求,所以会造成用户信息被迫修改,更严重者引发蠕虫攻击。
CSRF 攻击可以从站外和站内发起。从站内发起 CSRF 攻击,需要利用网站本身的业务,比如“自定义头像”功能,恶意用户指定自己的头像 URL 是一个修改用户信息的链接,当其他已登录用户浏览恶意用户头像时,会自动向这个链接发送修改信息请求。
从站外发送请求,则需要恶意用户在自己的服务器上,放一个自动提交修改个人信息的htm 页面,并把页面地址发给受害者用户,受害者用户打开时,会发起一个请求。
如果恶意用户能够知道网站管理后台某项功能的 URL,就可以直接攻击管理员,强迫管理员执行恶意用户定义的操作。
代码示例
一个没有 CSRF 安全防御的代码如下:
<?
$user=checkSQL($user);
$pass=checkSQL($pass);
$sql=“update UserTB set password=$user Where user=$pass”;
mysqli_stmt_execute($sql);
?>
代码中接收用户提交的参数“user,pass”,之后修改了该用户的数据,一旦接收到一个用户发来的请求,就执行修改操作。
提交表单代码:
<form action="http://localhost/servlet/modify" method="POST">
<input name="email">
<input name="tel">
</form>
当用户点提交时,就会触发修改操作。
攻击实例
如果“代码示例”中的代码,是 xxx.com 上的一个 web 应用,那么恶意用户为了攻击 xxx.com 的登录用户,可以构造 2 个 HTML 页面。
(1) 页面 a.htm 中,iframe 一下 b.htm,把宽和高都设为 0。
<iframe src="b.htm" width="0" height="0"></frame>
这是为了当攻击发生时,受害用户看不到提交成功结果页面。
(2) 页面 b.htm 中,有一个表单,和一段脚本,脚本的作用是,当页面加载时,自动提交这个表单。
<form id="modify" action="http://xxx.com/servlet/modify" method="POST">
<input name="email">
<input name="tel">
</form>
<script>
document.getElementById("modify").submit();
</script>
(3) 攻击者只要把页面 a.htm 放在自己的 web 服务器上,并发送给登录用户即可。用户打开 a.htm 后,会自动提交表单,发送给 xxx.com 下的那个存在 CSRF 漏洞的web 应用,所以用户的信息,就被迫修改了。
解决方案
CSRF 防御的原理是,在用户登录的时候,生成一个随机的 token,将它存储在 cookie中(默认情况,也可以放在 session 中),在生成表单时,生成一个隐藏域,隐藏域的值就等
于 token 的值。如果用户提交这个表单,就可以在接收用户请求的 web 应用中,判断隐藏域的 TOKEN 值是否和用户 COOKIE 中的 TOKEN 值一致,如果不一致或没有这个值,就判
断为 CSRF 攻击。攻击者无法预测每一个用户登录时生成的那个随机 TOKEN 值,所以无法伪造这个参数。
常见问题
(1) 为什么不直接验证 referer?
因为还有站内发出的 csrf,并且 referer 是可以被篡改的,是不可靠的数据
(2) 如果先发生 xss 攻击,攻击者可以拿到用户页面的 token 怎么办?
无解,请先做好 xss 防范。
文件包含
PHP 可 能 出 现文 件 包 含 的 函 数: include、 include_once 、 require、 require_once 、show_source、highlight_file、readfile、file_get_contents、fopen、file
防范方法:
对输入数据进行精确匹配,比如根据变量的值确定语言 en.php、cn.php,那么这两个文件放在同一个目录下'language/'.$_POST[‘lang'].'.php',
那么检查提交的数据是否是 en 或者 cn 是最严格的,检查是否只包含字母也不错,通过过滤参数中的/、..等字符。
HTTP 响应拆分
PHP 中可导致 HTTP 响应拆分的情况为:使用 header 函数和使用$_SERVER 变量。注意PHP 的高版本会禁止 HTTP 表头中出现换行字符,这类可以直接跳过本测试。
防范方法:
精确匹配输入数据
检测输入输入中如果有\r 或\n,直接拒绝
变量覆盖
PHP 变量覆盖会出现在下面几种情况:
遍历初始化变量
例:
foreach($_GET as $key => $value)
$$key = $value;
函数覆盖变量:parse_str、mb_parse_str、import_request_variables,Register_globals=ON 时,GET 方式提交变量会直接覆盖
防范方法:
设置 Register_globals=OFF
不要使用这些函数来获取变量
动态函数
当使用动态函数时,如果用户对变量可控,则可导致攻击者执行任意函数。
例:
<?php
$myfunc=$_GET['myfunc'];
$myfunc();
?>
防御方法:
不要这样使用函数
会话安全
HTTPOnly 设置
session.cookie_httponly = ON 时,客户端脚本(JavaScript 等)无法访问该 cookie,打开该指令可以有效预防通过 XSS 攻击劫持会话 ID
domain 设置
检查 session.cookie_domain 是否只包含本域,如果是父域,则其他子域能够获取本域的cookies
path 设置
检查 session.cookie_path,如果网站本身应用在/app,则 path 必须设置为/app/,才能保证安全
cookies 持续时间
检查 session.cookie_lifetime,如果时间设置过程过长,即使用户关闭浏览器,攻击者也会危害到帐户安全
secure 设置
如果使用 HTTPS,那么应该设置 session.cookie_secure=ON,确保使用 HTTPS 来传输cookies
session 固定
如果当权限级别改变时(例如核实用户名和密码后,普通用户提升到管理员),我们就应该修改即将重新生成的会话 ID ,否则程序会面临会话固定攻击的风险。
加密
明文存储密码
采用明文的形式存储密码会严重威胁到用户、应用程序、系统安全。
密码弱加密
使用容易破解的加密算法,MD5加密已经部分可以利用 md5破解网站来破解
参考方案
md5(md5($password).$salt)
密码存储在攻击者能访问到的文件
例如:保存密码在 txt 、ini、conf、inc、xml 等文件中,或者直接写在 HTML 注释中
认证和授权
用户认证
检查代码进行用户认证的位置,是否能够绕过认证,例如:登录代码可能存在表单注入。
检查登录代码有无使用验证码等,防止暴力破解的手段
函数或文件的未认证调用
一些管理页面是禁止普通用户访问的,有时开发者会忘记对这些文件进行权限验证,导致漏洞发生
某些页面使用参数调用功能,没有经过权限验证,比如 index.php?action=upload
密码硬编码
有的程序会把数据库链接账号和密码,直接写到数据库链接函数中。
随机函数
rand() VS mt_rand()
rand()最大随机数是 32767,当使用 rand 处理 session 时,攻击者很容易破解出session,建议使用mt_rand()。
代码示例
<?php
//on windows
print mt_getrandmax(); //2147483647
print getrandmax();// 32767
?>
可以看出 rand()最大的随机数是 32767,这个很容易被我们暴力破解。
<?php
$a= md5(rand());
for($i=0;$i<=32767;$i++){
if(md5($i) ==$a ) {
print $i."-->ok!!<br>";exit;
}else { print $i."<br>";}
}
?>
当我们的程序使用 rand 处理 session 时,攻击者很容易暴力破解出你的 session,但是对于 mt_rand 是很难单纯的暴力的。