過濾輸入:
是指轉(zhuǎn)義或者刪除不安全的字符。在數(shù)據(jù)到達(dá)應(yīng)用的儲(chǔ)存層(mysql or redis)之前,一定要過濾輸入的數(shù)據(jù)。
過濾HTML數(shù)據(jù):
使用htmlentities()函數(shù)。默認(rèn)情況下,這個(gè)函數(shù)不會(huì)轉(zhuǎn)義單引號(hào)。而且也檢測(cè)不出輸入字符串的字符集。htmlentities()函數(shù)的正確使用方式是:第一個(gè)參數(shù)是輸入字符串;第二個(gè)參數(shù)設(shè)定為ENT_QUOTES常量,轉(zhuǎn)義單引號(hào);第三個(gè)參數(shù)設(shè)定為輸入字符串的字符集。
Example:
<?php
$input = '<p><script>alert("You won the Nigerian lottery!");</script></p>';
echo htmlentities($input, ENT_QUOTES, 'UTF-8');
SQL查詢:
有時(shí)必須根據(jù)輸入數(shù)據(jù)構(gòu)建SQL查詢。直接拼接$_GET或者$_POST中的原始輸入數(shù)據(jù)會(huì)對(duì)數(shù)據(jù)庫(kù)造成巨大傷害。
要使用PDO預(yù)處理語句(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);
預(yù)處理語句會(huì)自動(dòng)過濾$email的值,防止數(shù)據(jù)庫(kù)收到SQL注入攻擊。一個(gè)SQL語句字符串中可以有很多個(gè)具名占位符,然后在預(yù)處理語句上調(diào)用bindValue()方法綁定各個(gè)占位符的值。
用戶資料信息:
應(yīng)用中如果有用戶資料信息,可能就需要處理郵件地址,電話,郵政編碼等資料信息。這種情況,則需要使用filter_var()和filter_input()函數(shù)。這兩個(gè)函數(shù)可以有效的過濾不同類型的輸入:電子郵件地址,URL編碼字符串,整數(shù)等。
Example:
<?php
$email = "victor.chen@sony.com<script>ALERT</script>";
$safeEmail = filter_var($email, FILTER_SANITIZE_EMAIL);
驗(yàn)證數(shù)據(jù):
在過濾數(shù)據(jù)之后,也需要驗(yàn)證數(shù)據(jù):
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";
}
轉(zhuǎn)義輸出:
把輸出渲染成網(wǎng)頁(yè)或者API響應(yīng)時(shí),一定要轉(zhuǎn)義輸出。這也是一種防護(hù)措施,能避免渲染惡意代碼,還能防止應(yīng)用的用戶無意之中執(zhí)行惡意代碼。
htmlentities函數(shù)可以轉(zhuǎn)義輸出,另外一些PHP模板引擎也能如smarty/smarty,會(huì)自動(dòng)轉(zhuǎn)義輸出。
使用bcrypt計(jì)算用戶密碼的哈希值:
我們應(yīng)該計(jì)算用戶密碼的哈希值,而不能加密用戶的密碼。加密和哈希不是一回事。加密是雙向算法,加密的數(shù)據(jù)以后可以解密。而哈希是單向算法,哈希后的數(shù)據(jù)不能再還原成原始值,而且相同的數(shù)據(jù)得到的哈希值始終相同。
數(shù)據(jù)庫(kù)中的密碼需要儲(chǔ)存成哈希值而不是明文。
最安全的哈希算法是bcrypt。
注冊(cè)用戶例子:
<?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();
}
?>
注意:密碼的哈希值要儲(chǔ)存再VARCHAR(255)類型的數(shù)據(jù)庫(kù)中。這樣便于以后存儲(chǔ)比現(xiàn)在的bcrypt算法得到的哈希值更長(zhǎng)的密碼。
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()函數(shù)需要接受兩個(gè)參數(shù),第一個(gè)是純文本密碼,第二個(gè)是用戶記錄中現(xiàn)有的密碼哈希值。如果函數(shù)返回的是true,則證明純文本密碼正確。
password_needs_rehash()函數(shù)檢查用戶記錄中現(xiàn)有的密碼哈希值是否需要更新。這個(gè)函數(shù)能確保指定的密碼哈希值是使用的最新的哈希算法選項(xiàng)創(chuàng)建的。
密碼的哈希值需要更新是因?yàn)楣V档淖饔靡蜃訒?huì)增長(zhǎng)(因?yàn)橛?jì)算機(jī)越來越快,作用因子不夠多會(huì)影響安全性)
日期,時(shí)間和時(shí)區(qū):
首先要為PHP中處理日期和時(shí)間的函數(shù)設(shè)置默認(rèn)時(shí)區(qū)??梢苑旁趐hp.ini文件中:
date.timezone = 'America/New_York';
也可以在運(yùn)行時(shí)使用date_default_timezone_set()函數(shù)設(shè)置默認(rèn)時(shí)區(qū)。
PDO擴(kuò)展:
PHP Data Objects (數(shù)據(jù)對(duì)象)是一系列PHP的類,抽象了不同的數(shù)據(jù)庫(kù)具體實(shí)現(xiàn)。
當(dāng)我們使用數(shù)據(jù)庫(kù)的時(shí)候,需要保證數(shù)據(jù)庫(kù)憑據(jù)的安全。應(yīng)該把數(shù)據(jù)庫(kù)憑據(jù)保存在一個(gè)位于文檔根目錄之外的配置文件中,然后在需要使用憑據(jù)時(shí)導(dǎo)入。千萬不能把憑據(jù)保存到公共的庫(kù)里,如github。
事務(wù):
PDO擴(kuò)展還支持事務(wù)。事務(wù)是指把一系列數(shù)據(jù)庫(kù)語句當(dāng)成個(gè)邏輯單元(具有原子性)執(zhí)行。也就是說,事務(wù)中的一系列SQL查詢要么都成功執(zhí)行,要么根本不執(zhí)行。事務(wù)的原子性能保證數(shù)據(jù)的一致性,安全性和持久性。事務(wù)還有個(gè)很好的副作用----提升性能,因?yàn)槭聞?wù)其實(shí)是把多個(gè)查詢排成隊(duì)列,一次性全部執(zhí)行。? ? ? ? ? ? ? ? ? ? ?
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();
?>
多字節(jié)字符串:
PHP假設(shè)字符串中每個(gè)字符都是八位字符,占一個(gè)內(nèi)存,可是很多語言使用的字符串會(huì)多于8位字符串。mbstring擴(kuò)展提供了處理多字節(jié)字符串,能代替大多數(shù)PHP原生的處理字符串的函數(shù)。如,mb_strlen()函數(shù)用于替代PHP原生的strlen()函數(shù)。
字符編碼:
所有的現(xiàn)代的WEB瀏覽器都能處理UTF-8字符編碼。字符編碼是打包Unicode數(shù)據(jù)方式,以便把數(shù)據(jù)存儲(chǔ)在內(nèi)存中,或者通過線纜在服務(wù)器和客戶端之間傳輸。
1. 一定要知道數(shù)據(jù)的字符編碼。
2. 使用UTF-8字符編碼存儲(chǔ)數(shù)據(jù)。
3. 使用UTF-8字符編碼輸出數(shù)據(jù)。
mbstring擴(kuò)展不僅能處理Unicode字符串,還能再不同的字符編碼之間轉(zhuǎn)換多字節(jié)字符串。如果客戶使用Windows專用的字符編碼到處Excel電子表格,我們可以通過mb_detect_encoding()和mb_convert_encoding()函數(shù)可以把Unicode字符串從一種字符串編碼成另一種字符編碼。
流:
流的作用是在出發(fā)地和目的地之間傳輸數(shù)據(jù),流為PHP的很多IO函數(shù)提供底層實(shí)現(xiàn),如file_get_contents(), fopen(), fgets()等。
流的封裝協(xié)議:
我們讀寫文件可以通過HTTP, HTTPS或者SSH與遠(yuǎn)程服務(wù)器通信,還可以打開讀寫ZIP,RAR,等壓縮文件。
1. 開始通信
2. 讀取數(shù)據(jù)
3. 寫入數(shù)據(jù)
4. 結(jié)束通信
每個(gè)流都有一個(gè)協(xié)議和一個(gè)目標(biāo)。指定協(xié)議和目標(biāo)的方法是使用流標(biāo)識(shí)符,其格式如下:
<scheme>://<target>
其中,<scheme>是流的封裝協(xié)議,<target>是流的數(shù)據(jù)源。
<?php
$json = file_get_contents(
? ? 'http://api.flickr.com/services/feeds/photo_public.gne?format=json'
);
這其中“http”協(xié)議會(huì)讓PHP使用http流封裝協(xié)議。http之后是流的目標(biāo)。這就是HTTP流封裝協(xié)議所規(guī)定的。
file://也是流封裝協(xié)議,不過我們通常可以省略因?yàn)檫@是PHP默認(rèn)的封裝協(xié)議
<?php
$handle = fopen('/etc/hosts', 'rb');? ?// 我們省略了file://流封裝協(xié)議
$handle = fopen('file://etc/hosts', 'rb');? ?// 我們標(biāo)注了file://流封裝協(xié)議。
fopen(), fgets(), fputs()等文件系統(tǒng)函數(shù)可以處理ZIP壓縮文件以及Amazon S3服務(wù)。
我們也可以自定義流,用來支持部分或全部PHP文件系統(tǒng)函數(shù)。
流過濾器:
stream_filter_append()函數(shù)可以把過濾器附加到流上。
<?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這個(gè)filter加到了流了輸出的全是大寫。
也可以使用php://filter來附加過濾器
<?php
$handle = fopen('php://filter/read=string.toupper/resource=data.txt', 'rb');
while (feof($handle) !== true) {
? ? echo fgets($handle);
}
fclose($handle);
?>
這個(gè)流標(biāo)識(shí)符如下:
filter/read=<filter_name>/resource=<scheme>://<target>
PHP某些文件系統(tǒng)函數(shù)在調(diào)用后無法附加過濾器,例如file(),所以這些函數(shù)使用時(shí)只能用php://filter封裝協(xié)議附加流過濾器。
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? ? ? 流來的桶隊(duì)列
? ? * @param resource $out? ? 流走的桶隊(duì)列
? ? * @param int? ? ? $consumed? ? 處理的字節(jié)數(shù)
? ? * @param bool? ? $closing? ? 是流中最后一個(gè)桶隊(duì)列么?
? ? */
? ? 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)) {
? ? ? ? ? ? // 檢查桶數(shù)據(jù)
? ? ? ? ? ? $bucket->data = str_replace($bad, $good, $bucket->data);
? ? ? ? ? ? // 增加已處理的數(shù)據(jù)量
? ? ? ? ? ? $consume += $backet->datalen;
? ? ? ? ? ? // 把桶放入流向下游隊(duì)列
? ? ? ? ? ? $stream_bucket_append($out, $bucket);
? ? ? ? }
? ? ? ? return PSFS_PASS_ON;
? ? }
}
?>
filter()方法的作用是接收,處理再轉(zhuǎn)運(yùn)桶中的數(shù)據(jù)流。在filter()方法中,我們迭代桶隊(duì)列$in中的桶,把臟字替換成審查后的值。這個(gè)方法的返回值是PSFS_PASS_ON常量,表示操作成功。
$in? ? 上游流來的一個(gè)隊(duì)列,有一個(gè)或多個(gè)桶,桶中是從出發(fā)地流來的數(shù)據(jù)
$out? ? 由一個(gè)桶或多個(gè)桶組成的隊(duì)列,流向下游的流目的地
&$consumed? ? 自定義的過濾器處理的流數(shù)據(jù)總字節(jié)數(shù)
$closing? ? ?filter()方法接受的是最后一個(gè)桶隊(duì)列么?
然后還需要注冊(cè)自定義的DirtyWordsFilter流過濾器
<?php
stream_filter_register('dirty_words_filter', 'DirtyWordsFilter');
第一個(gè)參數(shù)是用于識(shí)別這個(gè)自定義過濾器的過濾名,第二個(gè)參數(shù)是這個(gè)自定義過濾器的類名?,F(xiàn)在可以使用這個(gè)自定義的流過濾器。