Nsdd

A personal wiki, chronicling hacking, data, and AI learning.

Sec Coding - PHP

2023-02-03


[TOC]

PHP代码安全规范

1. 代码实现

1.1 输入验证

1.1.1 【必须】按类型进行数据校验

// bad: 未进行输入验证
if(isset($_GET['email'])){
    echo "<br>your email is <br>" . $_GET['email'];
}else{
    echo "<br>please input your email<br>";
}

// good: 根据数据类型、格式、组成等编写正则表达式进行输入验证
$pattern = "/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/";
if(isset($_GET['email'])){
    if(preg_match($pattern, $_GET['email'])){
        echo "<br>your email is <br>" . $_GET['email'];
    }else{
        echo "<br>input email is not valid<br>";
    }
    
}else{
    echo "<br>please input your email<br>";
}

1.2 执行命令

1.2.1 【必须】过滤传入命令执行函数的字符

// bad 未对输入进行验证和处理
if(isset($_GET['ip'])){
    $ip = $_GET['ip'];
    system("ping -c 2 " . $ip);
}

// good: 执行命令前,过滤/转义指定符号, 校验数据格式。
$black_arr = [
    "\n", "&", "|", "'", "\"", "\\", "`", "$", "(", ")", "-"
];
if(isset($_GET['ip'])){
    $ip = $_GET['ip'];
    foreach($black_arr as $key => $value){
        $ip = str_replace($value, "", $ip);
    }
    if(filter_var($ip, FILTER_VALIDATE_IP)){
        system("ping -c 2 " . $ip);
    }else{
        echo "ip is not valid";
    }
}
// good: 通过白名单,限定外部可执行命令范围
if(isset($_GET['cmd']) && isset($_GET['arg'])){
    switch($_GET['cmd']){
        case "ping":
            $ip = $_GET['arg'];
            if(filter_var($ip, FILTER_VALIDATE_IP)){
                system("ping -c 2 ". $ip);
            }
            break;
        case "nslookup":
            system("nslookup www.qq.com");
            break;
        default:
            echo "plz input cmd args";
            
    }
}
// good: 使用escapeshellarg处理传入的参数,处理过后的arg不可使用双引号进行包裹。
if(isset($_GET['arg'])){
    $arg = escapeshellarg($_GET['arg']);
    echo "" . $arg . "<br>";
    system("ping -c 2 " . $arg);
}

1.2.2 【建议】避免直接调用函数执行系统命令

// good: 通过白名单,限定外部可执行命令范围
if(isset($_GET['cmd']) && isset($_GET['arg'])){
    switch($_GET['cmd']){
        case "ping":
            $ip = $_GET['arg'];
            if(filter_var($ip, FILTER_VALIDATE_IP)){
                system("ping -c 2 ". $ip);
            }
        		break;
        case "nslookup":
            system("nslookup www.qq.com");
        		break;
    }
}

1.3 文件操作

1.3.1 【必须】文件类型限制

1.3.2 【必须】校验并限定文件路径范围,避免路径穿越

// bad:未检查文件名/路径
if(isset($_GET['filename'])){
    $path = "/var/www/html/" . $_GET['filename'];
    echo file_get_contents($path);
}
// good:检查了文件名/路径,是否包含路径穿越字符
if(isset($_GET['filename'])){
    $path = "/var/www/html/" . $_GET['filename'];
    if(strrpos($path, '..') === false){
        echo file_get_contents($path);
    }else{
        echo "filename is not valid";
    }
}

1.3.3 【必须】及时释放资源

对资源进行操作完毕后及时释放资源。

// good: 打开文件读取完毕后及时关闭文件
$myfile = fopen("webdictionary.txt", "r") or die("Unable to open file!");
echo fgets($myfile);
fclose($myfile);

1.3.4 【建议】避免路径拼接

1.3.5 【建议】上传文件存储到第三方系统

1.3.6 【建议】存储文件名随机化

1.3.7 【必须】避免使用PHP伪协议

1.4 网络请求

1.4.1 【必须】限定访问网络资源协议、地址范围

当程序需要从用户指定的URL地址获取网页文本内容加载指定地址的图片进行下载等操作时,需要对URL地址进行安全校验:

  1. 只允许HTTPHTTPS协议

  2. 解析目标URL,获取其HOST

  3. 解析HOST,获取HOST指向的IP地址转换成Long型

  4. 检查IP地址是否为内网IP

10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
100.64.0.0/10
9.0.0.0/8
127.0.0.0/8
11.0.0.0/8
30.0.0.0/8
21.0.0.0/8
22.0.0.0/8
26.0.0.0/8
28.0.0.0/8
29.0.0.0/8
  1. 请求URL
  2. 如果有跳转,跳转后执行1,否则对URL发起请求。

可使用SSRF_BLCAK_DECT进行安全请求。

1.4.2 【推荐】请求网络资源,应加密传输

1.5 响应输出

1.5.1 【必须】设置正确的HTTP响应包类型

1.5.2 【必须】设置安全的HTTP响应头

1.5.3 【必须】外部输入并且在响应页面展示的数据需进行编码处理

场景 编码规则
输出点在HTML标签之间 需要对以下6个特殊字符进行HTML实体编码(&, <, >, “, ‘,/)。
示例:
& –> &amp;
< –> &lt;
>–> &gt;
” –> &quot;
’ –> &#x27;
/ –> &#x2F;
输出点在HTML标签普通属性内(如href、src、style等,on事件除外) 要对数据进行HTML属性编码。
编码规则:除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为&#xHH;(以&#x开头,HH则是指该字符对应的十六进制数字,分号作为结束符)
输出点在JS内的数据中 需要进行js编码
编码规则:
除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为 \xHH (以 \x 开头,HH则是指该字符对应的十六进制数字)
Tips:这种场景仅限于外部数据拼接在js里被引号括起来的变量值中。除此之外禁止直接将代码拼接在js代码中。
输出点在CSS中(Style属性) 需要进行CSS编码
编码规则:
除了阿拉伯数字和字母,对其他所有的字符进行编码,只要该字符的ASCII码小于256。编码后输出的格式为 \HH (以 \ 开头,HH则是指该字符对应的十六进制数字)
输出点在URL属性中 对这些数据进行URL编码
Tips:除此之外,所有链接类属性应该校验其协议。禁止JavaScript、data和Vb伪协议。

1.5.4 【必须】响应禁止展示物理资源、程序内部代码逻辑等敏感信息

1.5.5 【建议】添加安全纵深防御措施

<?php
// good:正确配置Content-Type、添加了安全响应头,引入了CSP
header("Content-Type: application/json");
header("X-Content-Type-Options: nosniff");
header("X-Frame-Options: SAMEORIGIN");
header("Content-Security-Policy: script-src 'self'");

1.6 数据输出

1.6.1 【必须】敏感数据加密存储

1.6.2 【必须】敏感信息必须由后台进行脱敏处理

1.6.3 【必须】高敏感信息禁止存储、展示

1.6.4 【必须】个人敏感信息脱敏展示

在满足业务需求的情况下,个人敏感信息需脱敏展示。

脱敏范围参考 http://km.oa.com/group/31829/articles/show/313706 《IDC开发运维安全标准》5.3.2 的建议:

1.6.5 【必须】后台地址不对外开放

1.7 执行代码

1.7.1 【必须】安全的代码执行方式

1.8 Web跨域

1.8.1 【必须】限定JSONP接口的callback字符集范围

1.8.2 【必须】安全的CORS配置

if($_SERVER['HTTP_ORIGIN'] && $_SERVER['HTTP_ORIGIN'] === 'https://domain.qq.com'){
    header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']);
    header("Access-Control-Allow-Credentials: true");
}

1.9 SQL操作

1.9.1 【必须】SQL语句默认使用预编译并绑定变量

// bad:未使用参数绑定方式执行SQL查询
$id = $_GET['id'];
$sql = "SELECT * FROM pages WHERE id = $id";
$result = $mysql->query($sql);

// good: 使用PDO,基于参数绑定的方式执行SQL语句
$sth = $dbh->prepare('SELECT name, colour, calories
    FROM fruit
    WHERE calories < ? AND colour = ?');
$sth->execute(array(150, 'red'));
$red = $sth->fetchAll();

// good: 使用PDO,基于参数绑定的方式执行SQL语句
$sql = 'SELECT name, colour, calories
    FROM fruit
    WHERE calories < :calories AND colour = :colour';
$sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$sth->execute(array(':calories' => 150, ':colour' => 'red'));
$red = $sth->fetchAll();

1.9.2 【必须】安全地使用开发框架SQL查询方法

// bad:错误地拼接SQL语句
$dbResult = $this->db->query("SELECT * FROM users WHERE username = '".$_POST['user_name']."'");

// good:正确地使用CodeIgniter框架的where语句
$this->db->where('username',$this->input->post('user_name'));
$dbResult = $this->db->get('users');

// good:正确地使用CodeIgniter框架的参数绑定功能
$sql = "SELECT * FROM some_table WHERE id = ? AND status = ? AND author = ?";
$this->db->query($sql, array(3, 'live', 'Rick'));
// good:正确地使用Laravel框架提供的ORM方法
$someVariable = Input::get("some_variable");
DB::select("SELECT * FROM some_table WHERE some_col = :somevariable", array(
   'somevariable' => $someVariable,
));

1.10 NoSQL操作

1.10.1 【必须】校验参数值类型

<?php
$mongo = new MongoClient();    // 连接到mongodb
$db = $mongo->test; //选择数据库
$coll = $db->test; //选择集合
$username = $_GET['username'];
$password = $_GET['password'];

// bad: 在执行NoSQL操作之前,未做任何判断。
$data = array(
        'username'=>$username,
        'password'=>$password
        );
$data = $coll->find($data);
$count = $data->count();
if ($count>0) {
    foreach ($data as $user) {
        echo 'username:'.$user['username']."</br>";
        echo 'password:'.$user['password']."</br>";
    }
}
else{
    echo '未找到';
}

// good: 在进入NoSQL操作之前,判断输入的值是否为字符串。
if(gettype($username) !== "string"){
    echo "username must be a string";
    exit();
}
if(gettype($password) !== "string"){
    echo "password must be a string";
    exit();
}
$data = array(
        'username'=>$username,
        'password'=>$password
        );
$data = $coll->find($data);
$count = $data->count();
if ($count>0) {
    foreach ($data as $user) {
        echo 'username:'.$user['username']."</br>";
        echo 'password:'.$user['password']."</br>";
    }
}
else{
    echo '未找到';
}
?>

为什么要这么做?

在PHP中,因为PHP本身的弱类型特性,如果我们传入的参数是个数组,PHP会自动进行解析成数组形式。比如我们传入了一个?a=1,在$_GET这个数组中的表现形式就是Array('a'=>'1');,如果我们传入了一个?a[key]=value,那么在$_GET中的表现就是Array('a'=>array('key'=>'value'))

在上述例子中:

1.10.2 【必须】NoSQL操作前,应校验权限/角色

<?php
$mongo = new MongoClient();    // 连接到mongodb
$db = $mongo->test; //选择数据库
$coll = $db->test; //选择集合


if(isset($_GET['action']) && in_array($_GET['action'], array('add', 'del'))){
    $action = $_GET['action'];
    if(isset($_GET['id'])){
        $id = (int)($_GET['id']);
        // bad: 未进行权限校验,直接执行NoSQL操作
        $coll->remove(array("id" => $id));

        // good: 进行权限校验,用户身份验证通过后,执行NoSQL操作
        if(isset($_SESSION['admin']) && $_SESSION['admin'] === true){
            $coll->remove(array("id" => $id));
        }
    }
}

1.10.3 【必须】避免使用字符串拼接来执行NoSQL数据库的Shell命令

<?php
error_reporting(0);
if(isset($_POST["username"]) && isset($_POST["password"])){
    if(login($_POST["username"],$_POST["password"])){
        die("Good, can you find out my password?");
    }
    die("username or password error!");
}
else{
    highlight_file(__FILE__);
}


function login($u,$p){
    $manager = new MongoDB\Driver\Manager("mongodb://mongo:27017");
    // bad: 使用字符串拼接的方式构造执行的Shell操作。
    $q = '{"username": "'.$u.'", "password": "'.$p.'"}';
    $query = new MongoDB\Driver\Query(json_decode($q));
    $cursor = $manager->executeQuery('babyDB.user', $query);

    $data = [];
    foreach($cursor as $doc) {
        $data[] = $doc;
    }
    if (isset($data) && isset($data[0]->password)) {
        return true;
    }
    return false;
}
?>

1.11 URL跳转

1.12.1 【必须】限定跳转目标地址

// bad:直接取外部可控输入做跳转

$url = __USER_CONTROLLED_VALUE__;
header('Location: '. $url);

// good:跳转做前限定协议和域名白名单的检查

$url = "http://www.baidu.com";

function check_url($url){
	
	// 定义协议和域名白名单
	// 最佳情况下,host白名单应限制到指定的三级域名
	$scheme_white_list = array("http", "https");
	$host_white_list = array("www.qq.com");
	
	// PHP历史版本存在对URL的错误解析逻辑,将导致URL白名单检查被绕过
	// 请确保使用安全的PHP版本:php >=5.6.28, >=7.0.13
	// 参考:https://bugs.php.net/bug.php?id=73192
	$url_arr = parse_url($url);
	$scheme = isset($url_arr['scheme']) ? $url_arr['scheme'] : '';
	$host = isset($url_arr['host']) ? $url_arr['host'] : '';
			
	if (!in_array($scheme, $scheme_white_list)) {
		return false;
	}
	
	if (!in_array($host, $host_white_list)) {
		return false;
	}
	
	return true;
}

if (check_url($url)) {
	header('Location: '. $url);
} else {
	echo '非法重定向地址';
}

1.12 Cookie与登录态

1.12.1 【必须】为Cookies中存储的关键登录态信息添加http-only保护

1.12.2 【建议】接入ptlogin登录鉴权的业务,应使用p_skey票据

1.13 表单提交

1.13.1 【必须】启用框架级的CSRF防护机制

1.14 反序列化对象

1.14.1 【必须】输入不能作为unserialize的值

<?php

class Test{
    var $name;
    var $age;

    function __construct($name, $age){
        $this->name = $name;
        $this->age = $age;
    }

    function printData(){
        echo "My Name is $this->name, and I'm $this->age years old";
    }

}

// bad: 读取cookie中序列化的用户数据,直接反序列化成为对象。
$user_data = base64_decode($_COOKIE['user']);
$user_obj = unserialize($user_data); // 传入的user_data: O:4:"Test":2:{s:4:"name";s:4:"Tome";s:3:"age";i:13;}
$user_obj->printData();

// good: 使用安全的序列化数据格式json,接收用户传入的数据,并进行处理。
$user_data = json_decode(base64_decode($_COOKIE['user']));
$user_arr = json_decode($user_data, true); // 传入的user_data: {"name":"Tome","age":13}
$user_obj = new Test($user_arr['name'], $user_arr['age']);
$user_obj->printData();

1.14.2 【必须】反序列化外部读取的序列化数据之前,使用hash值进行校验

1.14.3 【必须】配置allowed_classes限定可序列化的类

在PHP7中,unserialize函数增加了一个allowed_classes参数用来指定可以反序列化的类,在使用PHP7进行开发过程中,应配置allowed_classes来将可以进行反序列化的类加入白名单,避免一些非预期的序列化数据传入导致安全风险。

1.15 XML读写

1.15.1 【必须】禁用外部实体

// good:加载XML前,禁用实体解析
libxml_disable_entity_loader(true);
$xml = simplexml_load_string($xmlContent);

2. 配置&环境

2.1 敏感配置

2.1.1 【必须】不在请求头中展示PHP信息

expose_php=Off

2.1.2 【必须】关闭PHP运行错误展示,记录错误日志

play_errors=Off
display_startup_errors=off log_errors=On error_log=/var/log/httpd/php_scripts_error.log

2.1.3 【必须】按需开启文件上传功能和最大上传文件限制

file_uploads=On upload_max_filesize=1M

2.1.4 【必须】配置disable_function禁用不必要的函数

2.1.5 【必须】使用低权限用户运行PHP

2.1.6 【必须】生产环境的安全框架配置