分类 PHP 下的文章

■ 密码设置

场景:用户注册、密码重置、密码找回时。

说明:密码设置时,应该满足8位及以上长度,含大小写字母、数字及特殊字符等的要求。用户密码设置必须经过后端校验,不允许设置不满足复杂度要求的密码。

PHP示例:后端验证密码复杂度。

<?php
    function validPwd($pwd) {
        if (strlen($pwd) < 8) {
            echo "密码必须包含至少含有8个字符,请返回修改!";
            return FALSE;
        }        
        if (preg_match_all('/[A-Z]/', $pwd, $o) < 1) {
            echo "密码必须包含至少一个大写字母,请返回修改!";
            return FALSE;
        }
        if (preg_match_all('/[a-z]/', $pwd, $o) < 1) {
            echo "密码必须包含至少一个小写字母,请返回修改!";
            return FALSE;
        }
        if (preg_match_all('/[0-9]/', $pwd, $o) < 1) {
            echo "密码必须包含至少一个数字,请返回修改!";
            return FALSE;
        }
        if (preg_match_all('/[~!@#$%^&*()\-_=+{};:<,.>?]/', $pwd, $o) < 1) {
            echo "密码必须包含至少一个特殊符号:[~!@#$%^&*()\-_=+{};:<,.>?],请返回修改!";
            return FALSE;
        }
        return TRUE;
    }
?>     

■ 密码存储

场景:用户提交的密码在服务器端保存时,需要做转换避免明文存储后泄露。

说明:用户密码存储时,应采用哈希算法(如SHA1)计算用户密码和唯一随机盐值(Salt)的摘要值,保存其摘要和Salt值,建议分开存储这两个值。

PHP示例:使用摘要算法BCrypt存储用户密码。

<?php
    try {
        $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
        if (!$email) {
            throw new Exception('非法的Email');
        }
    
        $password = filter_input(INPUT_POST, 'password');
        if (!$password || mb_strlen($password) < 8) {
            throw new Exception('密码长度必须大于8位');
        }
    
        $sql = "SELECT username FROM user WHERE username=:username";
        $stmt = $db->prepare($sql);
        $stmt->execute(array(
            ':username' => $email
        ));
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($row) {
            exit('用户Email已存在');
        }
    
        $passwordHash = password_hash(
           $password,
           PASSWORD_DEFAULT,
           ['cost' => 12]
        );
        if ($passwordHash === false) {
            throw new Exception('Password hash failed');
        }
        
        $sql_insert = "INSERT INTO `user` (username,password) VALUES (:username,:password)";
        $stmt = $db->prepare($sql_insert);
        $stmt->execute(array(
            ':username' => $email,
            ':password' => $passwordHash,
        ));
        $insert_id = $db->lastinsertid();
        if ($insert_id) {
            header('HTTP/1.1 302 Redirect');
            header('Location: login.html');
        }
    } catch (Exception $e) {
        header('HTTP/1.1 400 Bad request');
        echo $e->getMessage();
    }
?>
          
        

■ 密码修改

场景:密码修改。

说明:用户修改密码时,修改操作需通过手机号、账户或者邮箱进行二次身份验证。密码变更时,应短信或者邮件通知用户是否是本人操作,告知其安全风险。

PHP示例:

<?php
    session_start();
    try {
        $email = filter_input(INPUT_POST, 'email');
        $password = filter_input(INPUT_POST, 'password');
    
        $sql = "SELECT id,password FROM user WHERE username=:username";
        $stmt = $db->prepare($sql);
        $stmt->execute(array(
            ':username' => $email
        ));
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            exit('用户不存在');
        }
    
        if (password_verify($password, $row['password']) === false) {
            exit('密码错误!');
        }
    
        $_SESSION['user_reset'] = 'ok';
        $_SESSION['user_email'] = $email;
        header('HTTP/1.1 302 Redirect');
        header('Location: passwdReset.php');
    } catch (Exception $e) {
        header('HTTP/1.1 401 Unauthorized');
        echo $e->getMessage();
    }
?>
          
        

■ 密码找回

场景:密码找回

说明:用户密码找回时,后端需要对注册手机号或邮箱进行二次验证,验证码和验证链接应发送至预先注册的地址,并设置有效期以防止暴力破解。密保问题,应当支持尽可能随机的问题提问。在多个验证操作中,要对各验证机制进行排序,以防出现跳过前面验证机制直接到最后一步认证的安全风险。

示例【Java代码】:

<?php
    session_start();
    try {
        $email = filter_input(INPUT_POST, 'email');
        $sms_code = filter_input(INPUT_POST, 'sms_code');
    
        $sql = "SELECT id FROM user WHERE username=:username";
        $stmt = $db->prepare($sql);
        $stmt->execute(array(
            ':username' => $email
        ));
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            exit('用户不存在');
        }
    
        if (($sms_code === $_SESSION['sms_code']) == false) {
            exit('短信验证码错误!');
        }
    
        $_SESSION['user_pwdback'] = 'ok';
        header('HTTP/1.1 302 Redirect');
        header('Location: passwdReset.php');
    } catch (Exception $e) {
        header('HTTP/1.1 401 Unauthorized');
        echo $e->getMessage();
    }
?>

          
        

■ 密码使用

场景:密码使用。

说明:应用开发中禁止设置万能密码、硬编码明文的密码、使用数据库管理员账户操作、不同用户公用账户操作或者将密码输出到日志文件或者控制台。

PHP项目配置示例:config.ini。

datahost = 172.10.2.2
username = web
password = xxxxxx 

PHP示例:PHP包含使用。

<?php
    $arrs = parse_ini_file("config.ini", true);
    $conn = mysqli_connect($arrs['datahost'], $arrs['username'], $arrs['password']) or die(mysqli_connect_error());
    mysqli_select_db($conn, 'web');
    mysqli_query($conn, 'set names utf8');
    $sqls = 'SELECT * FROM users';
    $rest = mysqli_query($conn, $sqls);
    while ($row = mysqli_fetch_array($rest)) {
        print_r($row); 
    }
?>

■ SQL注入

场景:SQL注入发生在任何有用户可控输入并且会和数据库进行交互操作的功能处,比如:用户登录验证、查询、搜索、更新信息等。

说明:通过预编译实现参数化查询,将用户的输入数据和后台的SQL代码分离,正确的使用预编译来实现参数化查询,可以有效地防止SQL注入。

PHP 示例:使用PDO处理用户输入

<?php
    $host='localhost';
    $dbName='xxxxx';
    $user='xxxxxxx';
    $pass='xxxxxxx';
    $dsn="mysql:host=$host; dbname=$dbName";
    
    try {
        $pdo = new PDO($dsn, $user, $pass);
        if (version_compare(PHP_VERSION, '5.3.6', '<') && !defined('PDO::MYSQL_ATTR_INIT_COMMAND')) {
            $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
        }
    } catch (PDOException $e) {
        echo $e->getMessage();
        exit();
    }

    $pdo->exec("SET NAMES 'utf8'");
    $smt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    $smt->execute(array(':name' => $_GET['name']));
    // $smt->bindParam(':name', $_GET['name']); 
    foreach ($smt->fetchAll() as $row) {
        // do something with $row
    }
?>      

PHP 示例:使用Mysqli处理用户输入

<?php
    $host='localhost';
    $dbName='xxxxx';
    $user='xxxxxxx';
    $pass='xxxxxxx';
    
    try {
        $con = mysqli_connect($host, $user, $pass, $dbName)
        if (!&con) {
            echo mysqli_connect_error();
            exit();
        }
    } catch (PDOException $e) {
        echo $e->getMessage();
        exit();
    }
    $con->set_charset("utf8");
    $smt = $con->prepare("SELECT * FROM employees WHERE name = ?");
    $smt->bindParam("s", $_GET['name']);
    $smt->execute();
    $ret = $smt->get_result();
    while ($row = $ret->fetch_assoc()) {
        // do something with $row
    }
    mysqli_close($con);
?>  

■ XSS注入

场景:所有存在用户可控输入,而且会将用户输入输出到客户端的场景,比如:用户论坛发帖、用户评论、用户昵称、用户个人简介、用户提交反馈管理员后台查看、课程命名、课程信息介绍等等。

说明:对输入的数据进行过滤和转义,包含但不限于<>”’%()&+\’\”等危险特殊字符。

PHP示例:先对用户输入的内容输出进行html实体转码。

public function SafeFilter($arr){
    $ra=Array('/([\x00-\x08,\x0b-\x0c,\x0e-\x19])/','/script/','/javascript/','/vbscript/','/expression/','/applet/','/meta/','/xml/','/blink/','/link/','/style/','/embed/','/object/','/frame/','/layer/','/title/','/bgsound/','/base/','/onload/','/onunload/','/onchange/','/onsubmit/','/onreset/','/onselect/','/onblur/','/onfocus/','/onabort/','/onkeydown/','/onkeypress/','/onkeyup/','/onclick/','/ondblclick/','/onmousedown/','/onmousemove/','/onmouseout/','/onmouseover/','/onmouseup/','/onunload/');
    if (is_array($arr)){
        foreach ($arr as $key => $value){
            if (!is_array($value)){
                //不对magic_quotes_gpc转义过的字符使用addslashes(),避免双重转义。
                if (!get_magic_quotes_gpc()){
                    $value = addslashes($value);  //给单引号(')、双引号(")、反斜线(\)与 NUL(NULL 字符)加上反斜线转义
                }
                $value = preg_replace($ra,'',$value); //删除非打印字符,粗暴式过滤xss可疑字符串

                $arr[$key] = htmlentities(strip_tags($value)); //去除 HTML 和 PHP 标记并转换为 HTML 实体
            }else{
                SafeFilter($arr[$key]);
            }
        }
    }
}       

■ XML注入

场景:XML被设计用来传输和存储数据,所以XML注入发生在使用XML传输或存储数据,而对用户输入没有严格验证处理的场景。

说明:在XML文档内部或外部引用数据时,过滤用户提交的参数,如<、>、’、“、&等特殊字符。禁止加载外部实体,禁止报错。

PHP示例:假设一个在线商城可以由用户指定商品数量,来完成支付,会生成如下的XML:

<?php
    if (isset($_SERVER['HTTP_CLIENT_IP']) || isset($_SERVER['HTTP_X_FORWARDED_FOR']) || 
       !in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1',))) {
        header('HTTP/1.0 403 Forbidden');
        exit('You are not allowed to access this file.');
    }
?>          

title = ""
categories = "PHP"
date =

+++

■ 控制方法

场景:需要进行授权访问的系统在进行访问时接入。

说明:将访问控制的逻辑代码与应用程序其他代码分开,服务端根据会话标识来进行访问控制管理。

PHP示例:根据用户组对用户权限进行独立的访问控制。

<?php
    public function getPermission() {
        $this->is_login();
        $user_id = session('id');
        return $user_privileges = DB::table('userPermission')
                                  ->where('user_id', $user_id)
                                  ->get()
                                  ->toArray();
    }
?>

■ 控制管理

场景:用户访问权限校验。

说明:限制只有授权的用户才能访问受保护的URL、文件、服务、应用数据、配置、直接对象引用等。

PHP示例:用户访问URL时对其权限进行校验。

<?php
    namespace App\Http\Controllers\Data;
    use App\Http\Controllers\BaseController;
    
    public function preHandle() {
        $privileges = $this->getPermission();
        if (!$privileges) {
            return $this->responseError('没有访问权限');
        }
        // Handle logic here
    }
?>

■ 接口管理

场景:在那些具有安全性要求的应用程序中,需要检查应用程序是否能访问一些有限的资源,例如文件、套接字(socket) 等。

说明:限制只有授权的用户才能访问受保护的URL、文件、服务、应用数据、配置、直接对象引用等。

(coding...)

■ 权限变更

场景:权限发生变化时。

说明:当权限发生变更时,应记录日志,并通知用户是否是本人操作,告知存在的安全风险。

PHP示例:权限变更时通知。

<?php
    date_default_timezone_set('PRC');
     
    public function writeLog($filename, $msg) {
        $file = dirname(__FILE__) . '/logs/' . $filename . date('Ym') . '.log';
        !is_dir(dirname($file)) && mkdir(dirname($file), 0777, true);
        $handle = fopen($file, 'a');
        flock($handle, LOCK_EX);
        fwrite($handle, sprintf("%s %s\r\n", date('Y-m-d H:i:s',time()), $msg));
        flock($handle, LOCK_UN);
        fclose($handle);
    }
    
    public function roleUpdate() {
        require "DBDA.class.php";
        $db = new DBDA();
        $uid = $_POST["uid"];
        $js = $_POST["js"];
        
        $privileges = $this->getPermission();
        if (!$privileges) {
            return $this->responseError('没有操作权限');
        }
        
        $con = mysqli_connect($host, $user, $pass, $dbName)
        $con->set_charset("utf8");
        $smt = $con->prepare("DELETE FROM userRole WHERE userid=?");
        $smt->bindParam("s", $uid);
        $smt->execute();
        writeLog("admin", "DELETE FROM userRole WHERE userid=".$uid);
         
        $ajs = explode(",", $js);
        foreach($ajs as $v){
            $smt = $con->prepare("INSERT INTO userinjuese SET uid=?, value=?");
            $smt->bindParam("ss", $uid, $v);
            $smt->execute();
            writeLog("admin", "INSERT INTO userinjuese SET uid=".$uid.", value=".$v);
        }
    }
?>

■ 验证码生成

场景:用户注册、密码重置、密码找回时。

说明:复杂度至少4位数字或字母,或者采用拼图等验证方式,一次一用,建议有效期不超过180秒。 Java示例:后端生成验证码。

PHP 示例:后端生成验证码。

<?php
session_start();
$width = 100;
$height = 30;
$image = imagecreatetruecolor($width, $height);
$black = imagecolorallocate($image, 0, 0, 0);
$bgcolor = imagecolorallocate($image, 255, 255, 255);
imagefill($image, 0, 0, $bgcolor);
 
$captch_code = "";
for ($i=0; $i<4; $i++) {
    $fontsize = 6;
    $fontcolor = imagecolorallocate($image, rand(0, 100), rand(0, 100), rand(0, 100));
    $data = 'abcdefghijkmnpqrstuvwxyABCDEFGHIJKLMNOPQRSTUVWXYZ123456789';
    $fontcontent = substr($data, rand(0, strlen($data)-1), 1);
    $captch_code .= $fontcontent;
    $x = ($i*100/4) + rand(5, 10);
    $y = rand(5, 15);
    imagestring($image, $fontsize, $x, $y, $fontcontent, $fontcolor);
}
$_SESSION['captch_code'] = $captch_code;
 
for ($i<0; $i<200; $i++) {
    $pointcolor = imagecolorallocate($image, rand(50, 200), rand(50, 200), rand(50, 200));
    imagesetpixel($image, rand(1, $width-1), rand(1, $height-1), $pointcolor);
}
 
for ($i=0; $i<3; $i++) {
    $linecolor = imagecolorallocate($image, rand(100, 200), rand(100, 200), rand(100, 200));
    imageline($image, rand(1, $width-1), rand(1, $height-1), rand(1, $width-1), rand(1, $height-1), $linecolor);
}
 
header('Content-Type:image/png');
imagepng($image);
imagedestroy($image);
?>

html 用户登录页面增加验证码功能

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
    <form method="post">
        <table>
            <tr><td>用户名:</td><td><input type="text" name="username" placeholer="请输入用户名……"/></td></tr>
            <tr><td>密  码:</td><td><input type="password" name="password" /></td></tr>
            <tr><td>验证码:</td><td><input type='text' name='check' id='icode' value=''/><span id='checkcode'></span></td></tr>
            <tr><td><img id='captch' border='1' src="__CONTROLLER__/image?r=<?php echo rand();?>" width='100' heigth='30'/></td>
            <td><a href='javascript:void(0)' onclick="document.getElementById('captch').src='__CONTROLLER__/image?r='+Math.random()">看不清?</a></td></tr>
            <tr><td><input type="reset" value="取消" /></td><td><input type="submit" value="登录" /></td></tr>
        </table>
    </form>
    <script>
    document.getElementById("icode").onblur=function() {
        if (window.XMLHttpRequest) {
            request = new XMLHttpRequest();
        } else {
            request = new ActiveXObject("Microsoft.XMLHTTP");
        }
        var url="check?checkcode="+document.getElementById("icode").value;
        request.open('GET', url, true);
        request.send();
        request.onreadystatechange = function() {
            if (request.readyState == 4) {
                if (request.status == 200) { 
                    document.getElementById("checkcode").innerHTML = request.responseText;
                } else {
                    alert("错误:" + request.status);
                }
            } 
        }        
    }
    </script>
</body>
</html>

验证码校验功能实现。

<?php
    function checkCode($code, $userid) {
        session_start();
        if (!isset($_SESSION[static::MESSAGE_CODE_TIME]) || !isset($_SESSION[static::MESSAGE_CODE])) {
            echo "Error: code is null.";
            return -1;
        }
        if ($_SESSION[static::MESSAGE_CODE_TIME] + 60*3 < time()) {
            $this->unsetCode();
            echo "Error: code timeout.";
            return -1;
        }
        if ($_SESSION[static::MESSAGE_CODE] == $code && $_SESSION[static::MESSAGE_CODE_USER] == $userid) {
            $this->unsetCode();
            return 0;
        }
        echo "Error, check code fail.";
        return -1;
    }
?>

■ 验证码生成/验证码限制/安全提示/凭证校验

场景:用户注册、密码重置、密码找回时。

说明:复杂度至少6位数字或字母,一次一用,建议有效期不超过180秒。前后端设置用户获取频率为60秒一次,建议每个用户每天获取的短信最多10条。增加安全提示:至少含本次操作的功能、验证码发送编号、是否是个人自己操作的风险等信息。禁止在响应中返回验证码,服务器端同时校验密码、短信验证码等凭证信息,防止出现多阶段认证绕过的漏洞。

<?php
    function sendCode($phone) {
        session_start();
        $time = time();
        if ($_SESSION[static::MESSAGE_CODE_USER] && $_SESSION[static::MESSAGE_CODE_TIME] + 60 > $time) {
            echo "Error: send sms code too frequeue, try it later.";
            return -1;
        }
        if (!isset($_SESSION[static::SESSION_MESSAGE_CODE_COUNT])) {
            $_SESSION[static::SESSION_MESSAGE_CODE_COUNT] = 1;
        } else {
            $_SESSION[static::SESSION_MESSAGE_CODE_COUNT]++;
        }
        if ($_SESSION[static::SESSION_MESSAGE_CODE_COUNT] >= 5) {
            $this->unsetCode();
            echo "sms code send time more than five, try it later...";
            return -1;
        }
        setcookie(session_name(), session_id(), $time+60,"/");
        $phone = $_GET["phone"];
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, "http://apis.xxx.com/sms/send?mobile={$phone}&key={$key}");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $json = curl_exec($ch);
        $rets = json_decode($json, true);
        $_SESSION[static::MESSAGE_CODE_USER] = $phone;
        $_SESSION[static::MESSAGE_CODE] = $rets['smscode'];
        $_SESSION[static::MESSAGE_CODE_TIME] = $time;
        echo $rets['error_code'];
        return 0;
    }

    function checkCode($code, $phone) {
        session_start();
        if (!isset($_SESSION[static::MESSAGE_CODE_TIME]) || !isset($_SESSION[static::MESSAGE_CODE])) {
            echo "Error: sms code is null.";
            return -1;
        }
        if ($_SESSION[static::MESSAGE_CODE_TIME] + 60*3 < time()) {
            $this->unsetCode();
            echo "Error: sms code is timeout.";
            return -1;
        }
        if ($_SESSION[static::MESSAGE_CODE] == $code && $_SESSION[static::MESSAGE_CODE_USER] == $phone) {
            $this->unsetCode();
            echo "OK, check sms code success.";
            return 0;
        }
        echo "Error, check sms code fail.";
        return -1;
    }
?>