php安全编码规范
1.配置
php.ini基本安全配置
1.1应启用“cgi.force_redirect”
cgi.force_redirect在php.ini中修改,默认是开启的,它可以防止当PHP运行的CGI脚本未经验证的访问。在IIS,OmniHTTPD和Xitami上是禁用的,但在所有其他情况下它应该打开。
; php.ini
cgi.force_redirect=1 ;
1.2应禁用“enable_dl”
该指令仅对Apache模块版本的PHP有效。你可以针对每个虚拟机或每个目录开启或关闭dl()动态加载PHP模块。关闭动态加载的主要原因是为了安全。通过动态加载,有可能忽略所有open_basedir限制。默认允许动态加载,除了使用安全模式。在安全模式,总是无法使用dl()。
; php.ini
enable_dl=0 ;
1.3应禁用“file_uploads”
file_uploads默认是开启的,允许将文件上传到您的站点。因为来自陌生人的文件本质上是不可信甚至危险的,除非您的网站绝对需要,否则应禁用此功能。如果开启请进行相应的限制,参考upload_max_filesize, upload_tmp_dir,和post_max_size。
; php.ini
file_uploads = 0 ;
1.4通过“open_basedir”限制文件访问权限
open_basedir默认是打开所有文件,它将 PHP 所能打开的文件限制在指定的目录树,包括文件本身。本指令不受安全模式打开或者关闭的影响。当一个脚本试图用例如 fopen() ,include或者 gzopen() 打开一个文件时,该文件的位置将被检查。当文件在指定的目录树之外时 PHP 将拒绝打开它。所有的符号连接都会被解析,所以不可能通过符号连接来避开此限制。
open_basedir应该配置一个目录,然后可以递归访问。但是,应该避免使用. (当前目录)作为open_basedir值,因为它在脚本执行期间动态解析特殊值 . 指明脚本的工作目录将被作为基准目录,但这有些危险,因为脚本的工作目录可以轻易被 chdir() 而改变。
在 httpd.conf 文件中,open_basedir 可以像其它任何配置选项一样用“php_admin_value open_basedir none”的方法关闭(例如某些虚拟主机中)。在 Windows 中,用分号分隔目录。在任何其它系统中用冒号分隔目录。作为 Apache 模块时,父目录中的 open_basedir 路径自动被继承。
用 open_basedir 指定的限制实际上是前缀,不是目录名。也就是说“open_basedir = /dir/incl”也会允许访问“/dir/include”和“/dir/incls”,如果它们存在的话。如果要将访问限制在仅为指定的目录,用斜线结束路径名。例如:“open_basedir = /dir/incl/”。
; php.ini
open_basedir="${USER}/scripts/data" ;
1.5应禁用“session.use_trans_sid”
默认为 0(禁用)。当禁用cookie时,如果它开启,PHP会自动将用户的会话ID附加到URL。基于 URL 的会话管理比基于 cookie 的会话管理有更多安全风险,从表面上看,这似乎是让那些禁用cookie的用户正常使用您的网站的好方法。实际上,它使那些用户容易被任何人劫持他们的会话。例如用户有可能通过 email 将一个包含有效的会话 ID 的 URL 发给他的朋友,或者用户总是有可能在收藏夹中存有一个包含会话 ID 的 URL 来以同样的会话 ID 去访问站点。也可以从浏览器历史记录和服务器日志中检索URL获取会话ID。
; php.ini
session.use_trans_sid = 0 ;
1.6会话管理cookie不能是持久的
没有固定生命周期或到期日期的Cookie被称为非持久性或“会话”cookie,这意味着它们只会持续与浏览器会话一样长,并且在浏览器关闭时会消失。具有到期日期的Cookie叫做“持久性”Cookie,他们将被存储/保留到这些生存日期。
管理网站上的登录会话应用非持久性cookie。要使cookie非持久化,只需省略该 expires属性即可。也可以使用session.cookie_lifetime实现。
1.7应禁用"allow_url_fopen"和"allow_url_include"
allow_url_fopen和allow_url_include默认是开启的,他们允许代码从URL中读入脚本。从站点外部吸入可执行代码的能力,加上不完美的输入清理可能会使站点裸露给攻击者。即使该站点的输入过滤在今天是完美的,但不能保证以后也是。
; php.ini
allow_url_fopen = 0
allow_url_include = 0
2.编码
php安全编码建议
2.1慎用sleep()函数
sleep()有时用于通过限制响应率来防止拒绝服务(DoS)攻击。但是因为它占用了一个线程,每个请求需要更长的时间来服务,这会使应用程序更容易受到DoS攻击,而不是减少风险。
if (is_bad_ip($requester)) {
sleep(5); // 不合规的用法
}
2.2禁止代码动态注入和执行
eval()函数是一种在运行时运行任意代码的方法。 函数eval()语言结构是非常危险的,因为它允许执行任意 PHP 代码。因此不鼓励使用它。如果您仔细的确认过,除了使用此结构以外别无方法,请多加注意,不要允许传入任何由用户提供的、未经完整验证过的数据。
eval($code_to_be_dynamically_executed) // 不合规的用法
2.3禁止凭据硬编码
因为从编译的应用程序中提取字符串很容易,所以永远不应对凭证进行硬编码。对于分发的应用程序尤其如此。 凭据应存储在受强保护的加密配置文件或数据库中的代码之外。
// 合规的用法
$uname = getEncryptedUser();
$password = getEncryptedPass();
connect($uname, $password);
// 不合规的用法
$uname = "steve";
$password = "blue";
connect($uname, $password);
2.4禁止危险函数
有时候,我们不希望执行包括system()等在那的能够执行命令的php函数,或者能够查看phpinfo信息的
phpinfo()等函数,那么我们就可以禁止它们:
disable_functions = system,passthru,exec,shell_exec,popen,phpinfo
如果你要禁止任何文件和目录的操作,那么可以关闭很多文件操作
disable_functions = chdir,chroot,dir,getcwd,opendir,readdir,scandir,fopen,unlink,delete,copy,mkdir, rmdir,rename,file,file_get_contents,fputs,fwrite,chgrp,chmod,chown
以上只是部分常用的文件处理函数,你也可以把上面执行命令函数和这个函数结合,应该就能够抵制大部分的phpshell了。
3、常见漏洞安全编码
3.1 低版本框架、库漏洞
安全方法: 此类安全问题应使用稳定的高版本框架、库进行开发。
3.2 Command Injection
安全方法: 无法规避此类功能时,应使用白名单控制:
if(isset($_POST["target"]))
{
$target = $_POST["target"];
switch ($target) {
case "www.protect.domain": echo "" . shell_exec("nslookup www.protect.domain") . "";
break;
case "web.protect.domain": echo "" . shell_exec("nslookup web.protect.domain") . "";
break;
...
default: echo "" . shell_exec("nslookup www.protect.domain") . "";
}
}
3.3 SQL Injection
安全方法:
注意: 不论项目是否使用了框架,涉及到的表名、字段名用户可控时,均应先使用白名单对此类数据进行处理:
switch ($order){
case "id": $order="id";
break;
case "name":$order="name";
break;
default :$order="id";
}
a、未使用框架时,可使用提供的安全方法:
//查询
$result=$this->db->select()->from("table_name")->where("id","=",$id)->execute()->fetchAll();
//删除
$result=$this->db->delete()->from("table_name")->where("name","like","%".$name."%")->execute();
//插入
$result=$this->db->insert(array("name","age"))->into("table_name")->values(array($name,$age))->execute();
//更新
$result=$this->db->update(array("name" => $name))->table("table_name")->where("id", "=", $id)->execute();
b、使用Yii、laravel、CodeIgniter框架时,可使用框架自带的数据库访问方法。Yii 2.0
//查询
Users::find()->where(["id"=>$id])->orderBy("name")->select("id,name")->one();
//插入
$user=new Users();
$user->age=$age;
$user->name=$name;
$user->save();
//单条更新
$use=Users::find()->where(["id"=>$id])->orderBy("name")->select("id,name")->one();
$use->name=$name;
$use->save();
//多条更新
$result=Users::find()->where([">","id",$id])->orderBy("name")->select("id,name")->all();
foreach ($result as $item){
$item->name=$name;
$item->save();
}
//删除
Users::deleteAll(["id"=>$id]);
Laravel
//查询
DB::table("table_name")->where("id",$id)->orderBy("id")->get();
//插入
DB::table("table_name")->insert(["name" => $name, "age" => $age]);
//更新
DB::table("table_name")->where("id", $id)->update(["name" => $name]);
//删除
DB::table("table_name")->where("age", $age)->delete();
CodeIgniter
//查询
$result = $this->db->select("*")->from("table_name")->where("userid", $userid)->order_by("id")->get();
//插入
$this->db->insert("table_name", array("title" => $title, "userid" => $userid));
//更新
$this->db->where("id",$id)->update("table_name", array("name" => $title, "age" => $age));
//删除
$this->db->where("id", $id)->delete("table_name");
3.4 XSS
安全方法:a、用户可控数据不需存储直接响应的,应编码输出。
// html实体编码输出
$this->securityUtil->encodeForHTML($data);
// javascript编码输出
$this->securityUtil->encodeForJavaScript($data)
b、用户可控数据需存储展示或用于其他系统展示的,应过滤危险字符。
$this->securityUtil->purifier($_GET["data"]);
注意: 输入过滤会改变用户输入。应结合具体业务场景搭配使用输入过滤、输出编码。
3.5 CSRF
安全方法:
前端应从cookie中获取在认证通过后植入的csrf_token,并以POST方式提交包含csrf_token值的请求,前端代码如下:
function getCookie() {
var value = "; " + document.cookie;
var parts = value.split("; csrf_token=");
if (parts.length == 2)
return parts.pop().split(";").shift();
}
$.ajax({
type: "post",
url: "/xxxx",
data: {csrf_token:getCookie()},
dataType: "json",
success: function (data) {
if (data.ec == 200) {
//do something
}
}
});
后端应从POST请求体中提取csrf_token参数值,进行校验,代码如下:
if(!$this->securityUtil->verifyCSRFToken()){
return ; //csrf token 校验失败
}
// 开始处理业务逻辑
注意: 受csrf_token生成方式影响,当存在XSS时,会导致全局CSRF防护措施失效。
3.6 URL Redirect
安全方法: 此类安全问题服务端应根据具体的业务需求防止不安全的重定向:a、如果跳转后的链接比较少且比较固定,那么可以在服务端对参数进行白名单限制,非白名单里面的URL禁止跳转。
$index=intval($_GET["index"]);
switch($index){
case 1: $url="https://www.protect.domain/";
break;
case 2: $url="https://web.protect.domain/";
break;
...
default: $url="https://web.protect.domain/";
}
header("Location:".$url);
当链接比较多时,可根据索引从数据库检索。b、如仅希望在当前域跳转,或因业务需要,跳转的链接经常变化且比较多,应做个二次确认页,对非当前域的链接,提示用户将跳转到其他网站:
$white=[".protect.domain"];
//校验是否为信任域
if(!$this->securityUtil->verifyRedirectUrl($url,$white)){
// 非信任域名,提供二次确认页
}
// 开始处理业务逻辑
3.7 路径可控
安全方法: 无法规避外界指定路径名时,应使用白名单处理:
$directory = $_GET["directory"];
switch ($directory) {
// $directory重新赋值
case "./image":$directory="./image";
break;
case "./page":$directory="./page";
break;
...
default:$directory="./image";
}
while($line = readdir($directory))
{
//do something
}
3.8 Code Injection
安全方法: 无法规避此类功能时,应精确匹配用户提交数据:
$name=strval($_POST["name"]);
$regex="/^[a-zA-Z0-9]{3,20}$/";
if(preg_match($regex,$name,$matches) && $matches[0]===$name)
{
eval ("echo '" . $name . "';");
}
3.9 Xpath Injection
安全方法: 此类安全问题应精确匹配用户输入:
if(isset($_POST["login"]) && $_POST["login"]){
if(isset($_POST["password"]) && $_POST["password"]){
$login = strval($_POST["login"]);
$password = strval($_POST["password"]);
$xml = simplexml_load_file("./heroes.xml");
$regex="/^[a-zA-Z0-9]{3,20}$/";
if(preg_match($regex,$login,$match_login) && $match_login[0]===$login){
if(preg_match($regex,$password,$match_password) && $match_password[0]===$password){
$result = $xml->xpath("/heroes/hero[login= '" . $login . "' and password= '" . $password . "' ]");
//业务逻辑
}
}
}
}
3.10 资源泄露
安全方法: 此类安全问题应在相关操作完成后释放资源:
$file = fopen("file.txt", "w") or die("Unable to open file!");
...
//关闭由fopen()函数打开的文件
fclose($file);
3.11 XXE
安全方法: 此类安全问题应在解析XML数据时显式禁止加载外部实体:
//禁止加载外部实体
libxml_disable_entity_loader(true);
//解析xml数据
$xml = simplexml_load_string($data);
3.12 SSRF
安全方法: 应校验传入ip地址是否为内部ip:
if (!$this->securityUtil->verifySSRFURL($url)) {
return ; //内部ip
}
//开始处理业务逻辑
3.13 敏感信息泄露
安全方法: 此类安全问题应在代码上线之前删除注释信息(特别是敏感信息),合理设置忽略文件:
a、gitlab/github项目根目录新建文档 .gitignore,内容可参考.gitignore
b、SVN新建文档.svnignore,内容可参考.gitignore,执行:
svn propset svn:ignore -R -F .svnignore .
注意: 当使用add的时候,禁止使用:
svn add *
这样会把忽略中的文件也添加到仓库。应使如下命令:
svn add --force .
3.14 越权漏洞
安全方法: 涉及到用户数据的增删改查,应校验数据归属:
$articleId=$_GET["articleId"];
//用户登录状态下,从session取出用户唯一标识userid
$userId = session("userId");
...
//关联用户信息执行数据库操作
$stmt = $db->prepare("UPDATE articles SET del_flag=1 WHERE articleId=? ANS userId=?");
$stmt->bind_param("ss",$articleId, $userid);
$stmt->execute();
3.15 MongoDB Injection
安全方法: 涉及到MongoDB的操作,应禁用execute方法,校验数据类型:
$appId = $this->request->post("appId");
$num = $this->request->post("num");
...
//校验数据类型
if(!is_array($appId)&&!is_array($num))
{
$criteria = ["appId" => $appId, "num" => $num];
$ret = $this->mongodb->findOne($criteria);
}
预期数据类型明确的(如商品数量),应在接收用户提交数据时直接强制数据类型转换,避免因校验不严谨可能带来的其他问题。
3.16 弱类型漏洞
安全方法:
注意:a、涉及多字符串拼接进行md5运算时,应在各拼接字符串之间添加分隔符:
//避免md5("1234"."14"."234")===md5("1234"."1"."4234")
md5($secret . "|" . $uid . "|" . $code);
b、涉及到函数返回结果既可能是布尔值(FALSE),也可能是等同于布尔值(FALSE)的非布尔值且返回结果用于判断时,需显式判断执行结果
以内置函数strpos为例:
int strpos ( string $haystack , mixed $needle [, int $offset = 0 ] )
该函数:1)返回$needle在$haystack中首次出现的数字位置;2)未找到时返回布尔值FALSE;3)当$needle在$haystack起始位置出现时,会返回0.
则安全编码应为:
$domain="https://www.protect.domain";
//禁止 if(!strpos($domain,"https")) 或 if(strpos($domain,"https")!=false)的方式
if(strpos($domain,"https")!==false){
//处理业务逻辑
}
此类安全问题应使用=== 代替 == , !== 代替 !=。
if (isset($_GET["id"]) && isset($_GET["userId"])) {
$id = strval($_GET["id"]);
$userId = strval($_GET["userId"]);
//防止出现 md5("240610708")==md5("QNKCDZO")
if (md5($id) !== md5($userId)) {
return ;
}
//业务逻辑
}
3.17 任意文件上传
安全方法: 此类安全问题应校验上传文件大小、后缀、类型等是否符合要求:
$config=array('limit'=>5 * 1024 * 1024, //允许上传的文件最大大小
'type'=>array( //允许的上传文件后缀及MIME
"gif"=>"image/gif",
"jpg"=>"image/jpeg",
"png"=>"image/png")
);
$file = $_FILES["file"];
$data=$this->securityUtil->verifyUploadFile($file, $config);
if($data['flag']!==true){
return; //上传失败
}
//生成新的文件名拼接$data['ext']上传到文件服务器
3.18 本地文件包含
安全方法: 无法规避外界指定文件名时,应使用白名单处理:
$filename =$_GET["filename"];
switch ($filename) {
case "lfi.txt":include("./lfi.txt");
break;
...
default:include("./notexists.txt");
}
3.19 并发问题
安全方法:php+mysql(InnoDB、REPEATABLE-READ)
此类安全问题在已使用事务的前提下,应使用悲观锁或乐观锁解决:
悲观锁:
mysqli_query($conn, "BEGIN");
$rs = mysqli_query($conn, "SELECT num FROM oversold WHERE id = 1 FOR UPDATE "); //for UPDATE
$row = mysqli_fetch_array($rs);
$num = $row[0];
if($num>0)
{
//do something
mysqli_query($conn, "UPDATE oversold SET num = num - 1 WHERE id = 1");
}
if(mysqli_errno($conn)) {
mysqli_query($conn, "ROLLBACK");
} else {
mysqli_query($conn, "COMMIT");
}
mysqli_close($conn);
SELECT ... FOR UPDATE加悲观锁,保证总是获取最新的数据,适合写入频繁的场景.
乐观锁: 需使用一个新的字段version保存版本号:
mysqli_query($conn, "BEGIN");
$result = mysqli_query($conn, "SELECT num,version FROM oversold WHERE id = 1 ");
$row = mysqli_fetch_array($result);
$num = $row[0];
$version=$row[1];
if($num>0)
{
//do something
$stmt=$conn->prepare("UPDATE oversold SET num = num - 1,version=version+1 WHERE id = 1 AND version= ? ");
$stmt->bind_param("i",$version);
$stmt->execute();
}
if(mysqli_errno($conn)) {
mysqli_query($conn, "ROLLBACK");
} else {
mysqli_query($conn, "COMMIT");
}
mysqli_close($conn);
乐观锁比较适合数据修改比较少,读取比较频繁的场景。
注意: 悲观锁会带来比较大的性能开销,而乐观锁可能会读取到脏数据,具体采用哪种加锁方式可根据具体业务场景确定。
php+redis
当redis版本不小于2.6.12时,应使用set指令限流避免并发问题。set指定用法如下:
redis > SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
● EX seconds - 设置指定的过期时间,单位为秒。
● PX milliseconds - 设置指定的过期时间,单位为毫秒。
● NX - 仅当KEY不存在时,设置KEY的值为VALUE。
● XX - 仅当KEY存在时,设置KEY的值为VALUE。
代码示例:
$res=$redis->set($key, $value, ["nx", "px"=>$ps]);
if(!$res){
throw new Exception("操作太快了,请稍后再试!");
}
//开始处理业务逻辑
以上代码,在$ps毫秒内,对指定的$key,仅允许执行一次业务逻辑。
注意: 涉及到的key应结合具体业务场景注意key过期时间的设置,防止key的膨胀。
3.20 WebSocket跨站劫持
安全方法: 此类安全问题服务端应校验Origin头:
$origin="https://www.protect.domain/";
...
function CheckOrigin(){
if (array_key_exists("Origin", $_SERVER)) {
$value = $_SERVER["Origin"];
if($origin===$value){
return ture;
}
}
return false;
}