2021年8月

■ Token使用

场景:在进行一些转账、订单操作、编辑删除文章、修改用户信息等重要操作的时候,需要确认当前操作请求是否由真实的用户主动发起的。

说明:在重要操作的表单中增加会话生成的Token字段,一次一用,提交后在服务端校验该字段。

PHP示例:页面包含生成token:

<?php
    session_start();
    function set_token() {
        $_SESSION['token'] = md5(time()+rand(1, 1000));
    }
?>

<form method="POST">
    <input name="message" type="text">
    <input type="hidden" name="token" value="<?=_SESSION['token']?>">
    <input type="submit" />
</form>
          
        

■ 二次验证

场景:在进行一些转账、订单操作、编辑删除文章、修改用户信息等重要操作的时候,需要确认当前操作请求是否由真实的用户主动发起的。

说明:在关键表单提交时,要求用户进行二次身份验证,如密码、图片验证码、短信验证码等。

PHP示例:验证token的有效性:

<?php
    session_start();
    function check_token() {
        if (isset($_POST['token']) && $_POST['token'] === $_SESSION['token']) {
            return true;
        } else {
            return false;
        }
    }
    
    if (isset($_SESSION['token']) && check_token()) {
        echo "success";
    } else {
        echo "failed";
    }
    set_token();
?>
          
        

■ Referer验证

场景:同上

说明:检验用户请求中Referer字段是否存在跨域提交的情况。

PHP示例:检查请求连接的referer是否合法

<?php
    session_start();
    if (strpos($_SERVER['HTTP_REFERER'], 'www.xxx.com')  !== false) {
        exit("fail");
    } else {
        // process bussiness here.
    }
?>
          
        

■ 防止会话劫持

场景: 系统登录入口。

说明: 在应用程序进行身份验证时,建议持续使用HTTPS连接,认证站点使用HTTPS协议。如果连接是从HTTP跳转到HTTPS,需要重新生成会话标识符。禁止在HTTP和HTTPS之间来回转换,这可能会导致会话被劫持。

Nginx 强制使用 https

server {
    listen 80;
    server_name www.maksim.website;
    rewrite ^(.*) https://$server_name$1 permanent;
}

■ 会话标识符安全

场景: 系统登录验证过程。

说明: 会话标识符应放置在HTTP或HTTPS协议的头信息中,禁止以GET参数进行传递、在错误信息和日志中记录会话标识符。

PHP示例: 使用cookie保存会话标识符。

<?php
    session_start();
    $username = $_POST['username'];
    $password = sha1($_POST['password']);
    $remember = $_POST['remember'];
    $validatecode = $_POST['validateCode'];
    $ref_url = $_GET['req_url'];
     
    if ($validatecode != $_SESSION['checksum']) {
        exit("验证码不正确");
    } elseif ($username == '' || $password == '') {
        exit("用户名和密码都不能为空");
    } else {
        $row = getUserInfo($username, $password);
        if (empty ($row)) {
            exit("用户名和密码都不正确");
        } else {
            $_SESSION['user_info'] = $row;
            if (!empty($remember)) {
                setcookie("username", $username, time()+60*60*24, "/home/www/");
            }
            if (strpos($ref_url, "login.php") === false) {
                header("location:" . $ref_url );
            } else {
                header("location:main_user.php");
            }
        }
    }
?>
          
        

■ Cookie安全设置/会话有效期

场景:用户通过认证后生成会话标识符时进行安全配置。

说明:设置会话Cookie时,正确设置“HttpOnly”属性(禁止程序如JS脚本等读取Cookie信息);“Secure”属性(禁止Cookie通过HTTP连接传递到服务器端进行验证);“Domain“属性(跨域访问时可指定的授权访问域名) ,“Path“属性(授权可访问的目录路径)。会话应在平衡风险和功能需求的基础上设置有效期。定期生成一个新的会话标识符并使上一个会话标识符失效,这可以缓解那些因原会话标识符被盗而产生的会话劫持风险。

PHP示例:在生成cookie时进行配置。

<?php
    class CookieUtil {
        function getCookie($cName) {
            if (isset($_COOKIE[$cName])) { 
                return $_COOKIE;
            } else {
                return false;
            }
        }
        function getValue($cName) {
            if (isset($_COOKIE[$cName])) { 
                return $_COOKIE[$cName];
            } else {
                return false;
            }
        }
        function addCookie($cName, $cValue) {
            setcookie($cName, $cValue, time()+3600*24, "/", ".maksim.website");
            return true;
        }
        function updateCookie($cName, $cValue) {
            setcookie($cName, $cValue, time()+3600*24, "/", ".maksim.website");
            return true;
        }
        function elCookie($cName, $cValue) {
            setcookie($cName, "", time()-3600, "/", ".maksim.website");
            return true;
        }
    }
?>             

■ 会话注销

场景:会话注销时处理。

说明:注销功能应用于所有受身份验证保护的网页,用户登出后应立即清理会话相关信息,终止相关的会话连接。

PHP示例:在生成cookie时进行配置。

<?php
    session_start();
    if ($_GET['action'] == "logout") {
        unset($_SESSION['user_info']);
        echo '注销登录成功!';
        header("location:login.php");
    }
?>
        

■ 客户端保存

场景:在客户端需要保存用户信息时,为了防止泄露需要对其进行加密处理。

说明:客户端保存敏感信息时,禁止其表单中的自动填充功能,以明文形式保存敏感信息。

用户信息使用AES对称加密算法加密。

■ 服务器保存

场景:服务器端处理敏感信息后,需要及时清除防止泄露信息。

说明:服务端保存敏感信息时,禁止在程序中硬编码敏感信息,明文存储用户密码、身份证号、银行卡号、持卡人姓名等敏感信息,临时写入内存或文件中的敏感数据,应及时清除和释放。

PHP示例:可以及时清理敏感数据。

<?php
    session_start();
    $username = $_POST['username'];
    $password = $_POST['password'];
    if ($username == '' || $password == '') {
        exit("用户名和密码都不能为空");
    } else {
        $row = getUserInfo($username, $password);
        unset($username);
        unset($password);
        if (empty($row)) {
            exit("用户名和密码都不正确");
        } else {
            $_SESSION['user_info'] = $row;
            if (!empty($remember)) {
                setcookie("username", $username, time()+60*60*24, "/home/www/");
            }
            header("location:" . $ref_url );
        }
    }
?>

■ 服务端保存-禁止在代码中硬编码敏感信息

场景:代码中要进行连接数据库等涉及到敏感信息操作的时候,要避免使用硬编码,应将敏感信息放到配置文件单独保存,需要的时候读取使用。

说明:服务端保存敏感信息时,禁止在程序中硬编码敏感信息。

下面的代码将IP直接写到代码中,攻击者可以通过反编译java类文件得到这些敏感信息。

class IPaddress {
    public $ipAddress = "172.16.254.1";
}
  

正确的代码示例:从serveripaddress.txt中读取ip地址,然后再进行其他操作。

下面为错误实现方式,直接采用了硬编码,将用户名和密码直接写到了代码中:

public getConnection()  {
    return DriverManager.getConnection("mysql://localhost/dbName","username", "password");
}    

正确的实现方式:将用户名密码等重要信息写到配置文件中,使用的时候从配置文件中读取对应信息。

■ 服务端保存-禁止明文存储密码等信息

场景:服务端对用户密码等敏感信息进行存储的场景。

说明:服务端保存敏感信息时,禁止明文存储用户密码、身份证号、银行卡号、持卡人姓名等敏感信息。

PHP 示例:定义专门的密码哈希方法hashPassword(),在对密码进行操作时,调用该方法先对密码进行处理。

■ 密码设置

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

说明:密码设置时,应该满足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.');
    }
?>