良好实践

过滤输入:

是指转义或者删除不安全的字符。在数据到达应用的储存层(mysql or redis)之前,一定要过滤输入的数据。

过滤HTML数据:

使用htmlentities()函数。默认情况下,这个函数不会转义单引号。而且也检测不出输入字符串的字符集。htmlentities()函数的正确使用方式是:第一个参数是输入字符串;第二个参数设定为ENT_QUOTES常量,转义单引号;第三个参数设定为输入字符串的字符集。

Example:

<?php

$input = '<p><script>alert("You won the Nigerian lottery!");</script></p>';

echo htmlentities($input, ENT_QUOTES, 'UTF-8');


SQL查询:

有时必须根据输入数据构建SQL查询。直接拼接$_GET或者$_POST中的原始输入数据会对数据库造成巨大伤害。

要使用PDO预处理语句(prepared statement)。

<?php

$sql = 'SELECT id FROM users WHERE email = :email';

$statement = $pdo->prepare($sql);


$email = filter_input(INPUT_GET, 'email');

$statement->bindValue(':email', $email);

预处理语句会自动过滤$email的值,防止数据库收到SQL注入攻击。一个SQL语句字符串中可以有很多个具名占位符,然后在预处理语句上调用bindValue()方法绑定各个占位符的值。


用户资料信息:

应用中如果有用户资料信息,可能就需要处理邮件地址,电话,邮政编码等资料信息。这种情况,则需要使用filter_var()和filter_input()函数。这两个函数可以有效的过滤不同类型的输入:电子邮件地址,URL编码字符串,整数等。

Example:

<?php

$email = "victor.chen@sony.com<script>ALERT</script>";

$safeEmail = filter_var($email, FILTER_SANITIZE_EMAIL);


验证数据:

在过滤数据之后,也需要验证数据:

Example:

<?php

$email = "victor.chen@sony.com<script>ALERT</script>";

$isEmail = filter_var($email, FILTER_VALIDATE_EMAIL);

if ($isEmail !== false) {

    echo "Success";

} else {

echo "Fail";

}


转义输出:

把输出渲染成网页或者API响应时,一定要转义输出。这也是一种防护措施,能避免渲染恶意代码,还能防止应用的用户无意之中执行恶意代码。

htmlentities函数可以转义输出,另外一些PHP模板引擎也能如smarty/smarty,会自动转义输出。


使用bcrypt计算用户密码的哈希值:

我们应该计算用户密码的哈希值,而不能加密用户的密码。加密和哈希不是一回事。加密是双向算法,加密的数据以后可以解密。而哈希是单向算法,哈希后的数据不能再还原成原始值,而且相同的数据得到的哈希值始终相同。

数据库中的密码需要储存成哈希值而不是明文。

最安全的哈希算法是bcrypt。


注册用户例子:

<?php

try {


    $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);

    if (!$email) {


        throw new Exception("Invalid Email");

    }

    $password = filter_input(INPUT_POST, 'password');

    if (!$password || mb_strlen($password) < 8) {


        throw new Exception("Password must contain 8+ characters");

    }

    $passwordHash = password_hash(

        $password,

        PASSWORD_DEFAULT,

        ['cost' => 12]

    );

    if ($passwordHash === false) {


        throw new Exception("Password hash failed");

    }

    $user = new User();

    $user->email = $email;

    $user->password_hash = $passwordHash;

    $user->save();

    header('HTTP/1.1 302 Redirect');

    header('Location: /login.php');

} catch (Exception $e) {

    header('HTTP/1.1 400 Bad request');

    echo $e->getMessage();

}

?>

注意:密码的哈希值要储存再VARCHAR(255)类型的数据库中。这样便于以后存储比现在的bcrypt算法得到的哈希值更长的密码。


Login的示例:

<?php

session_start();

try {

    $email = filter_input(INPUT_POST, 'email');

    $password = filter_input(INPUT_POST, 'password');

    $user = User::findByEmail($email);

    if (password_verify($password, $user->password_hash) === false) {


        throw new Exception("Invalid Password", 1);

    }

    $currentHashAlgorithm = PASSWORD_DEFAULT;

    $currentHashOptions = array('cost' => 15);

    $passwordNeedsRehash = password_needs_rehash(

        $user->password_hash,

        $currentHashAlgorithm,

        $currentHashOptions 

    );

    if ($passwordNeedsRehash === true) {

        $user->password_hash = password_hash(

            $password,

            $currentHashAlgorithm,

            $currentHashOptions

        );

        $user->save();

    }

    $_SESSION['user_logged_in'] = 'yes';

    $_SESSION['user_email'] = $email;

    header('HTTP/1.1 302 Redirect');

    header('Location: /user-profile.php');

} catch (Exception $e) {

    header('HTTP/1.1 401 Unauthorized');

    echo $e->getMessage();

}

?>

password_verify()函数需要接受两个参数,第一个是纯文本密码,第二个是用户记录中现有的密码哈希值。如果函数返回的是true,则证明纯文本密码正确。

password_needs_rehash()函数检查用户记录中现有的密码哈希值是否需要更新。这个函数能确保指定的密码哈希值是使用的最新的哈希算法选项创建的。

密码的哈希值需要更新是因为哈希值的作用因子会增长(因为计算机越来越快,作用因子不够多会影响安全性)


日期,时间和时区:

首先要为PHP中处理日期和时间的函数设置默认时区。可以放在php.ini文件中:

date.timezone = 'America/New_York';

也可以在运行时使用date_default_timezone_set()函数设置默认时区。


PDO扩展:

PHP Data Objects (数据对象)是一系列PHP的类,抽象了不同的数据库具体实现。

当我们使用数据库的时候,需要保证数据库凭据的安全。应该把数据库凭据保存在一个位于文档根目录之外的配置文件中,然后在需要使用凭据时导入。千万不能把凭据保存到公共的库里,如github。


事务:

PDO扩展还支持事务。事务是指把一系列数据库语句当成个逻辑单元(具有原子性)执行。也就是说,事务中的一系列SQL查询要么都成功执行,要么根本不执行。事务的原子性能保证数据的一致性,安全性和持久性。事务还有个很好的副作用----提升性能,因为事务其实是把多个查询排成队列,一次性全部执行。                     

Example:

<?php

require 'settings.php';

// PDO Connection

try {

    $pdo = new PDO(

        sprintf(

            'mysql:host=%s;dbname=%s;port=%s;charset=%s',

            $settings['host'],

            $settings['name'],

            $settings['port'],

            $settings['charset'],

        ),

        $settings['username'],

        $settings['password']

    );

} catch (PDOException $e) {

    // Failed to connect

    echo "Database connection failed";

    exit;

}

// Query

$stmtSubstract = $pdo->prepare('

    UPDATE accounts

    SET amount = amount - :amount

    WHERE name = :name

');

$stmtAdd = $pdo->prepare('

    UPDATE accounts

    SET amount = amount + :amount

    WHERE name = :name

');

// Start the transaction

$pdo->beginTransaction();

// Withdrawal money from the account

$fromAccount = 'Checking';

$withdrawal = 50;

$stmtSubstract->bindParam(':name', $fromAccount);

$stmtSubstract->bindParam(':amount', $withdrawal, PDO::PARAM_INT);

$stmtSubstract->execute();

// Save money to account 2

$toAccount = 'Savings';

$deposit = 50;

$stmtAdd->bindParam(':name', $toAccount);

$stmtAdd->bindParam(':amount', $deposit, PDO::PARAM_INT);

$stmtAdd->execute();

// submit transaction

$pdo->commit();

?>


多字节字符串:

PHP假设字符串中每个字符都是八位字符,占一个内存,可是很多语言使用的字符串会多于8位字符串。mbstring扩展提供了处理多字节字符串,能代替大多数PHP原生的处理字符串的函数。如,mb_strlen()函数用于替代PHP原生的strlen()函数。


字符编码:

所有的现代的WEB浏览器都能处理UTF-8字符编码。字符编码是打包Unicode数据方式,以便把数据存储在内存中,或者通过线缆在服务器和客户端之间传输。

1. 一定要知道数据的字符编码。

2. 使用UTF-8字符编码存储数据。

3. 使用UTF-8字符编码输出数据。

mbstring扩展不仅能处理Unicode字符串,还能再不同的字符编码之间转换多字节字符串。如果客户使用Windows专用的字符编码到处Excel电子表格,我们可以通过mb_detect_encoding()和mb_convert_encoding()函数可以把Unicode字符串从一种字符串编码成另一种字符编码。


流:

流的作用是在出发地和目的地之间传输数据,流为PHP的很多IO函数提供底层实现,如file_get_contents(), fopen(), fgets()等。

流的封装协议:

我们读写文件可以通过HTTP, HTTPS或者SSH与远程服务器通信,还可以打开读写ZIP,RAR,等压缩文件。

1. 开始通信

2. 读取数据

3. 写入数据

4. 结束通信

每个流都有一个协议和一个目标。指定协议和目标的方法是使用流标识符,其格式如下:

<scheme>://<target>

其中,<scheme>是流的封装协议,<target>是流的数据源。

<?php

$json = file_get_contents(

    'http://api.flickr.com/services/feeds/photo_public.gne?format=json'

);

这其中“http”协议会让PHP使用http流封装协议。http之后是流的目标。这就是HTTP流封装协议所规定的。

file://也是流封装协议,不过我们通常可以省略因为这是PHP默认的封装协议

<?php

$handle = fopen('/etc/hosts', 'rb');   // 我们省略了file://流封装协议

$handle = fopen('file://etc/hosts', 'rb');   // 我们标注了file://流封装协议。

fopen(), fgets(), fputs()等文件系统函数可以处理ZIP压缩文件以及Amazon S3服务。

我们也可以自定义流,用来支持部分或全部PHP文件系统函数。


流过滤器:

stream_filter_append()函数可以把过滤器附加到流上。

<?php

$handle = fopen('data.txt', 'rb');

stream_filter_append($handle, 'string.toupper');

while (feof($handle) !== true) {

    echo fgets($handle);

}

fclose($handle);

?>

这里stream_filter_append把string.toupper这个filter加到了流了输出的全是大写。

也可以使用php://filter来附加过滤器

<?php

$handle = fopen('php://filter/read=string.toupper/resource=data.txt', 'rb');

while (feof($handle) !== true) {

    echo fgets($handle);

}

fclose($handle);

?>

这个流标识符如下:

filter/read=<filter_name>/resource=<scheme>://<target>

PHP某些文件系统函数在调用后无法附加过滤器,例如file(),所以这些函数使用时只能用php://filter封装协议附加流过滤器。


Example:

<?php

$dateStart = new \DateTime();  // current date

$dateInterval = \DateInterval::createFromDateString('-1 day');  // get date interval

$datePeriod = new \DatePeriod($dateStart, $dateInterval, 30);  // get the period between dates

foreach ($datePeriod as $date) {

    $file = "sftp://USER:PASS@rsync.net" . $date->format('Y-m-d') . 'log.bz2';

    if (file_exists($file)) {

        $handle = fopen($file, 'rb');  // read the file into stream

        stream_filter_append($handle, 'bzip2.decompress');  // add a filter to stream to decompress the zip file

        while (feof($handle) !== true) {

            $line = fgets($handle);

            if (strpos($line, 'www.example.com') !== false) {      // if find the address in the line

                fwrite(STDOUT, $line);      // output the line

            }

        }

        fclose($handle);

    }

}

?>


自定义流过滤器:

<?php

class DirtyWordsFilter extends php_user_filter

{

    /**

    * @param resource $in      流来的桶队列

    * @param resource $out    流走的桶队列

    * @param int      $consumed    处理的字节数

    * @param bool    $closing    是流中最后一个桶队列么?

    */

    public function filter($in, $out, &$consumed, $closing)

    {

        $words = array('grime', 'dirt', 'grease');

        $wordData = array();

        foreach ($words as $word) {

            $replacement = array_fill(0, mb_strlen($word), '*');

            $wordData[$word] = implode('', $replacement);

        }

        $bad = array_keys($wordData);

        $good = array_values($wordData);

        while ($bucket = stream_bucket_make_writeable($in)) {

            // 检查桶数据

            $bucket->data = str_replace($bad, $good, $bucket->data);

            // 增加已处理的数据量

            $consume += $backet->datalen;

            // 把桶放入流向下游队列

            $stream_bucket_append($out, $bucket);

        }

        return PSFS_PASS_ON;

    }

}

?>

filter()方法的作用是接收,处理再转运桶中的数据流。在filter()方法中,我们迭代桶队列$in中的桶,把脏字替换成审查后的值。这个方法的返回值是PSFS_PASS_ON常量,表示操作成功。

$in    上游流来的一个队列,有一个或多个桶,桶中是从出发地流来的数据

$out    由一个桶或多个桶组成的队列,流向下游的流目的地

&$consumed    自定义的过滤器处理的流数据总字节数

$closing     filter()方法接受的是最后一个桶队列么?

然后还需要注册自定义的DirtyWordsFilter流过滤器

<?php

stream_filter_register('dirty_words_filter', 'DirtyWordsFilter');

第一个参数是用于识别这个自定义过滤器的过滤名,第二个参数是这个自定义过滤器的类名。现在可以使用这个自定义的流过滤器。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容

  • 系列笔记:Modern PHP 笔记(一):语言特性Modern PHP 笔记(二):良好实践Modern PHP...
    郝开心信札阅读 911评论 0 5
  • 重写 -isEqual: 必须重写 -hash,因为实现哈希需要这个方法配合--遥想当年定义了一个Model的属性...
    子达如何阅读 230评论 0 3
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 27,375评论 1 45
  • 攻击者无时无刻不在准备对你的 Web 应用程序进行攻击,因此提高你的 Web 应用程序的安全性是非常有必要的。幸运...
    代码技巧阅读 672评论 0 9
  • 以前健身房还在的时候,我今天去明天不去的,特别惰性,总想着来日方长,还有很长时间可以挥霍,谁能想到噩耗说来就来,周...
    Cassiel小星星阅读 222评论 0 1