分类 PHP 下的文章

■ 网络限制

场景:内外部调用的接口,如果可以属于内部系统间或第三方合作的资源调用情况。

说明:调用方网络限制,比如通过防火墙、主机host 和Nginx deny等技术措施进行校验。

配置示例:使用网络防火墙配置IP白名单。

iptables -I INPUT -s 10.1.24.12/32 -p tcp -m tcp --dport 8080 -j ACCEPT
在生产环境不建议使用 iptables 的方式进行限制,因为这样会增加服务器的维护成本,在迁移服务器时,如果缺少文档或者管理人员疏忽,会导致环境不一致的情况。

使用Nginx deny配置IP白名单:

location / {
    allow  10.1.24.12/32;
    deny all;
}

■ 身份认证

场景:验证调用者身份。

说明:调用方身份认证,比如Key、Secret、证书等技术措施进行校验,禁止共享凭证。

Java示例:校验Key等是否有效。

public class APIInterceptor extends HandlerInterceptorAdapter { 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {       
        ...
        String accessKeyId = request.getParameter("AccessKeyId");
        if (validKeyId(accessKeyId) == false) 
            return false;
        ...
    }   
    private boolean validKeyId(String AccessKeyId) {
        User user = getUserById(AccessKeyId).getPermission;
        if (user)
            return true;
        return true;
    }
    ...
}
          
        

PHP 示例:校验Key等是否有效。

function verifyAccessKey($key) {
    $secret = getSecretByAccessKey($Key);
    if ($secret && getPermission($secret)) {
        return $secret;
    }
    return null;        
}
          
        

■ 完整性校验

场景:验证参数没有被修改。

说明:调用数据安全,对全部参数使用SHA1等摘要运算进行数字签名,识别数据被篡改。

Java示例:通过SHA1验证参数完整性。

public class APIInterceptor extends HandlerInterceptorAdapter { 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {       
        String SignatureNonce = request.getParameter("SignatureNonce");
        if (SignatureNonce == null)
            return false;         
            ...
        Enumeration paraKeys = request.getParameterNames();
        String encodeStr = "";
        while (paraKeys.hasMoreElements()) {
            String paraKey = (String) paraKeys.nextElement();
            if (paraKey.equals("SignatureNonce"))
                break;
            String paraValue = request.getParameter(paraKey);
            encodeStr += paraValue;
        }
        encodeStr += Default.TOKEN_KEY;
        if (!SignatureNonce.equals(DigestUtils.sha1Hex(encodeStr))) {
            response.setStatus(500);
            return false;
        }
        return true;
    }
}
          
        

PHP 示例:通过SHA1验证参数完整性。

function verifySign($secret, $data)
{
    if (!isset($data['sign']) || !$data['sign']) {
        echo 'sign error.';
        return false;
    }
    $sign = $data['sign'];
    unset($data['sign']);
    ksort($data);
    
    $params = http_build_query($data);
    if ($sign == sha1($params.$secret)) {
        echo 'request success.';
        return true;
    } else {
        echo 'request fail.';
        return false;
    }
}
          
        

■ 合法性校验

场景:接收到参数是否完整、是否存在重放攻击、越权问题等。

说明:调用的参数检查,如参数是否完整,时间戳和Token是否有效,调用权限是否合法等。

Java示例:验证参数完整。

public class APIInterceptor extends HandlerInterceptorAdapter { 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {       
        ...         
        String accessKeyId = request.getParameter("AccessKeyId");
        if (validKeyId(accessKeyId) == false) 
            return false;
        String token = request.getParameter("Token");   
        if (validToken(token) == false) 
            return false;
        String value1 = request.getParameter("param1");
        String value2 = request.getParameter("param2");
        if (value1 == null || value2 == null) 
            return false;       
        String url = request.getRequestURL().toString();
        if (validRequest(url, AccessKeyId) == false)
            return false;   
        return true;
    }   
    private boolean validRequest(String url, String AccessKeyId) {
        Permission permission = getUserById(AccessKeyId).getPermission;
        userUrlPattern = permission.getUrl();
        if (url.contains(userUrlPattern))
            return true;
        return false;
    }
}
          
        

PHP 示例:验证参数完整。

function verifyParams($data) {
    if (!isset($data['token']) || !$data['token']) {
        echo 'token null.';
        return false;
    }
    if (!validToken($data['token'])) {
        echo 'token error.';
        return false;
    }
    
    if (!isset($data['param1']) || !$data['param1']) {
        echo 'para1 null.';
        return false;
    }
    if (!isset($data['param2']) || !$data['param2']) {
        echo 'param2 null.';
        return false;
    }
    
    return true;
}
          
        

■ 可用性要求

场景:对请求频率和次数进行限制。

说明:调用服务要求,调用满足等幂性即保持数据一致性,对调用频率和有效期进行限制。

Java示例:限制代码如下:

public class APIInterceptor extends HandlerInterceptorAdapter { 
    private long timeStamp = getNowTime();
    private static int reqCount = 0;
    private final int limit = 100;
    private final long interval = 1000;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {               
        String timeStamp = request.getParameter("TimeStamp");
        if (validTime(timeStamp) == false)
            return false;
        if (notFrequent() == false)
            return false;
        ...
    }
    private boolean validTime(String timeStamp) {
        if (nowStamp - timeStamp < 3*60)
            return true;
        return false;
    }   
    public boolean notFrequent() {
        long now = getNowTime();
        if (now < timeStamp + interval) {
            reqCount++;
            return reqCount <= limit;
        } else {
            timeStamp = now;
            reqCount = 1;
            return true;
        }
    }
}
          
        

PHP 示例:限制代码如下:

function verifyTimeStamp($data, $_SESSION) {
    if (!isset($data['timestamp']) || !$data['timestamp']) {
        echo 'timestamp error.';
        return false;
    }
    if (time() - $data['timestamp'] > 180) {
        echo 'timestamp timeout.';
        return false;
    }
    
    if ($_SESSION['reqCount'] < 100) {
        $_SESSION['reqCount'] += 1;
        return true;
    } else if (time() > $_SESSION['starttime'] + 3600) {
        $_SESSION['reqCount'] = 1;
        $_SESSION['starttime'] = time();
    } else {
        echo 'request frequeuent.';
        return false;
    }
    
    return true;
}
          
        

■ 异常处理

场景:记录请求的日志。

说明:调用的异常处理,调用行为实时检测,发现异常及时阻拦。

Java示例:根据记录日志对异常IP进行封禁。

public class APIInterceptor extends HandlerInterceptorAdapter { 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 
        ...      
        logger.info(request);
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        logger.info(request);
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 
        logger.info(request); 
    }   
}
         
        

PHP 示例:根据记录日志对异常IP进行封禁。

<?php
    session_start();
    $data = $_POST;
    if (verifyTimeStamp($data, $_SESSION)) {
        error_log("verifyTimeStamp error!", 0);
        exit();
    }

    secret = verifyAccessKey($data['key'])
    if (!secret) {
        error_log("verifyAccessKey error!", 0);
        exit();
    }

    if (!verifySign($secret, $data)) {
        error_log("verifySign error!", 0);
        exit();
    }

    if (!verifyParams($data)) {
        error_log("verifyParams error!", 0);
        exit();
    }
    // process data here
?>
         
        

建议:在代码中使用“抛异常”还是“返回错误码”,对于公司外的 http/api 开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess、“错误码”、“错误简短信息”。

■ 合法性校验

场景:允许用户自定义上传文件的场景比如:用户头像上传,各种图片、文件、文档等资料上传。

说明:进行文件上传时,在服务端对文件属性进行合法性校验,白名单形式检查文档类型(如文件的后缀名、文件头信息校验等)和大小(图片校验长、宽和像素等)。

PHP 示例:通过白名单限制上传的文件类型,文件大小限制防止DDoS攻击。

<?php    
    if ($_FILES['myfile']['error'] > 0) {
        echo "文件上传出现错误";
        exit();
    }

    $file_type = $_FILES['myfile']['type'];
    if ($file_type != "image/jpeg" && $file_type != 'image/pjpeg') {
        echo "文件类型只能为jpg格式";
        exit();
    }
    
    $file_size = $_FILES['myfile']['size'];
    if ($file_size > 2*1024*1024) {
        echo "文件过大,不能上传大于2M的文件";
        exit();
    }

    if (is_uploaded_file($_FILES['myfile']['tmp_name'])) {
        $uploaded_file = $_FILES['myfile']['tmp_name'];
        $file_true_name = $_FILES['myfile']['name'];
        $move_to_file = $_SERVER['DOCUMENT_ROOT']."/upload/".time().rand(1, 1000).substr($file_true_name, strrpos($file_true_name, "."));
        if (move_uploaded_file($uploaded_file, iconv("utf-8", "gb2312", $move_to_file))) {
            echo "文件上传成功";
        }
    }
    echo "文件上传失败";
    exit();
?>

■ 存储环境设置

场景:同上

说明:进行文件保存时,保存在与应用环境独立的文档服务器中(配置独立域名),保存的目录为web启动用户所属,设置为不可同时有写和执行权限。

配置示例:

[root~ ]# chmod 755 /data/xxx.100tal.com/upload -Rf

■ 隐藏文件路径

场景:允许用户自定义上传文件的场景比如:用户头像上传,各种图片、文件、文档等资料上传。

说明:进行文件保存时,成功上传的文件需要进行随机化重命名,禁止给客户端返回保存的路径信息。

PHP示例:白名单检测文件后缀名,然后随机数重写文件名

<?php
//coding
          
        

■ 文件访问设置

场景:允许用户上传文件,并提供文件下载功能的场景,比如下载课件知识资料等

说明:进行文件下载时,应以二进制附件形式下载,建议不提供直接访问(防止木马文件直接执行)。

PHP 示例:

<?php
    $filename = $_GET['filename'];
    $download_path = $_SERVER['DOCUMENT_ROOT']."/upload/";
 
    if (eregi("\.\.", $filename)) {
        echo "抱歉,你不能下载该文件!";
        exit();
    }

    $file = str_replace("..", "", $filename);
    if (!eregi("\.jpeg$", $file)) {
        echo "抱歉,你不能下载该文件!";
        exit();
    }

    $file = "$download_path$file";
    if (!file_exists($file)) {
        echo "抱歉,文件不存在!";
        exit();
    }

    $type = filetype($file);
    header("Content-type: $type");
    header("Content-Disposition: attachment;filename=$filename");
    header("Content-Transfer-Encoding: binary");
    header('Pragma: no-cache');
    header('Expires: 0');
    set_time_limit(180);
    readfile($file);
?>
          
        

■ 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(),在对密码进行操作时,调用该方法先对密码进行处理。