譯自:《Pro PHP Security》
即使是最普通的字母數字輸入也可能是危險的,列舉幾個容易引起安全問題的字符:
! $ ^ & * ( ) ~ [ ] \ | { } ' " ; < > ? - `
在數據庫中可能有特殊意義的字符:
' " ; \
還有一些非打印字符:
字符\x00或者說ASCII 0,NULL或FALSE
字符\x10和\x13,或者說ASCII 10和13,\n \r
字符\x1a或者說ASCII 26,表示文件的結束
輸入錯誤的參數類型,也可能導致程序出現意想不到的錯誤。
輸入過多的參數值,可能導致溢出等錯誤。
這里特別要注意php.ini中的register_globals的設定,在早期的php版本中是默認開啟的,這會導致很嚴重的安全問題:
#!php
<?php
// set admin flag
if ($auth->isAdmin()) {
$admin = TRUE;
}
// ...
if ($admin) {
// do administrative tasks
}
?>
上面這段代碼看起來是安全的,但是如果register_globals開啟了的話,在訪問的url中加入?admin=1即可繞過前半部分的邏輯判斷。
更安全的代碼應該給$admin賦默認FALSE值:
#!php
<?php
// create then set admin flag
$admin = FALSE;
if ($auth->isAdmin()) {
$admin = TRUE;
}
// ...
if ($admin) {
// do administrative tasks
}
?>
早期人們開發調試的時候發現使用register_globals有極大的便利,所以早期的php版本中默認開啟。
但是隨著越來越多的安全問題,從php 4.2.0開始,register_globals變為了默認關閉。
當你發現register_globals是on的時候,你可能會在腳本當中加入如下代碼使其關閉:
#!php
ini_set('register_globals', 0);
但實際上,當所有的全局變量已經創建了之后,以上代碼并不會起到作用。
但是你可以在文檔的根目錄下的.htaccess的文件中加上下面這一行:
php_flag register_globals 0
變量聲明:強烈建議總是事先聲明變量。
檢查輸入的類型,長度和格式:
字符串檢查:在PHP中,字符串幾乎可以是任何事情,但有些值并不是嚴格的字符串類型,可以用is_string()函數來確定。
有些時候你不介意數字作為字符串,可以用empty()函數。
數字類型檢查:使用is_int()函數或者is_integer()或is_long(),例如:
#!php
$year = $_POST['year'];
if (!is_int($year))
exit("$year is an invalid value for year!");
也可以使用gettype()函數判斷類型后做處理:
#!php
if (gettype($year) != 'integer') {
exit("$year is an invalid value for year!");
}
至少還有三種方式可以吧$year變量轉變為整數:
#!php
$year = intval($_POST['year']);
$year = ( int ) $_POST['year'];
if (!settype($year, 'integer')) {exit("$year is an invalid value for year!");}
如果允許浮點型與零的值,可以使用is_numeric()函數來做判斷。 判斷一個值是否為布爾型的時候使用is_bool()函數。
下表是對各種類型變量使用各函數判斷的結果:
檢查字符串的長度使用strlen()變量:
#!php
if (strlen($year) != 4)
exit("$year is an invalid value for year!");
概括總結一下PHP中類型,長度,格式等驗證:
#!php
<?php
// set up array of expected values and types
$expected = array(
'carModel' => 'string',
'year' => 'int',
'imageLocation' => 'filename'
);
// check each input value for type and length
foreach ($expected AS $key => $type) {
if (empty($_GET[$key])) {
${$key} = NULL;
continue;
}
switch ($type) {
case 'string':
if (is_string($_GET[$key]) && strlen($_GET[$key]) < 256) {
${$key} = $_GET[$key];
}
break;
case 'int':
if (is_int($_GET[$key])) {
${$key} = $_GET[$key];
}
break;
case 'filename':
// limit filenames to 64 characters
if (is_string($_GET[$key]) && strlen($_GET[$key]) < 64) {
// escape any non-ASCII
${$key} = str_replace('%', '_', rawurlencode($_GET[$key]));
// disallow double dots
if (strpos(${$key}, '..') === TRUE) {
${$key} = NULL;
}
}
break;
}
if (!isset(${$key})) {
${$key} = NULL;
}
}
// use the now-validated input in your application
對于一些可能有害的字符,可以用如下的幾種方式進行保護:
使用 \ 對其進行轉義。
使用引號把他引起來。
使用%nn的方式編碼,如urlencode()函數。
可以嘗試在php.ini中開啟magic_quotes_gpc,這樣對于所有由用戶GET、POST、COOKIE中傳入的特殊字符都會轉義。
也可是使用addslashes()函數,但是開啟magic_quotes_gpc所造成的影響可能遠超過益處。
addslashes()也只對最常見的四個字符做了轉義:單引號、雙引號、反斜線、空字符。
同時為了使數據還原,需要使用stripslashes()函數,也可能破壞一些多字節的轉義。
推薦使用mysql_real_escape_string()函數,雖然只是用來設計轉義插入數據庫的數據,但是他能轉義更多的字符。
如:NULL、\x00、\n、\r、\、'、"和\x1a。使用用例:
#!php
<?php
$expected = array(
'carModel',
'year',
'bodyStyle'
);
foreach ($expected AS $key) {
if (!empty($_GET[$key])) {
${$key} = mysql_real_escape_string($_GET[$key]);
}
}
?>
使用mysql_real_escape_string()函數也不是萬能的,轉義一些并非是要寫入mysql的數據庫的數據可能不會產生作用或者出現錯誤。
可以根據自己的實際需要,自己使用str_replace()函數寫一個針對特殊字符的轉義。
文件名中不能包含二進制數據,否則可能引起問題。
一些系統允許Unicode多字節編碼的文件名,但是盡量避免,應當使用ASCII的字符。
雖然Unix系統幾乎可以在文件名設定中使用任何符號,但是應當盡量使用 - 和 _ 避免使用其他字符。
同時需要限定文件名的長度。
php中的文件操作通常使用fopen()函數與file_get_contents()函數。
#!php
<?php
$applicationPath = '/home/www/myphp/code/';
$scriptname = $_POST['scriptname'];
highlight_file($applicationPath . $scriptname);
?>
上面代碼的問題在于用戶POST輸入的scriptname沒有做任何過濾,如果用戶輸入../../../../etc/passwd,則有可能讀取到系統的passwd文件。
#!php
<?php
$uri = $_POST['uri'];
if (strpos($uri, '..'))
exit('That is not a valid URI.');
$importedData = file_get_contents($uri);
如果發現 .. 字符串就不執行會不會出現問題呢?如果前面并沒有路徑限制的話,仍然會出現問題:
使用file協議,當用戶輸入file:///etc/passwd的時候,會把passwd的內容帶入$importedData變量中。
1、接收一個由用戶提交的變量,假設變量為$variety:
#!php
$variety = $_POST['variety'];
2、接收的變量帶入構造一個數據庫查詢語句:
#!php
$query = "SELECT * FROM wines WHERE variety='$variety'";
3、把構造好的語句提交給MySQL服務器查詢,MySQL返回查詢結果。
當由用戶輸入lagrein' or 1=1#時,產生的結果將會完全不同。
檢查用戶輸入的類型,當用戶輸入的為數字時可以使用如下方式:
使用is_int()函數(或is_integer()或is_long()函數)
使用gettype()函數
使用intval()函數
使用settype()函數
檢查用戶輸入字符串的長度使用strlen()函數。
檢查日期或時間是否是有效的,可以使用strtotime()函數
對于一個已經存在的程序來說,可以寫一個通用函數來過濾:
#!php
function safe($string)
{
return "'" . mysql_real_escape_string($string) . "'";
}
調用方式:
#!php
$variety = safe($_POST['variety']);
$query = "SELECT * FROM wines WHERE variety=" . $variety;
對于一個剛開始寫的程序,應當設計的更安全一些,PHP5中,增加了MySQL支持,提供了mysqli擴展:
PHP手冊地址:http://php.net/mysqli
#!php
<?php
// retrieve the user's input
$animalName = $_POST['animalName'];
// connect to the database
$connect = mysqli_connect('localhost', 'username', 'password', 'database');
if (!$connect)
exit('connection failed: ' . mysqli_connect_error());
// create a query statement resource
$stmt = mysqli_prepare($connect, "SELECT intelligence FROM animals WHERE name = ?");
if ($stmt) {
// bind the substitution to the statement
mysqli_stmt_bind_param($stmt, "s", $animalName);
// execute the statement
mysqli_stmt_execute($stmt);
// retrieve the result...
mysqli_stmt_bind_result($stmt, $intelligence);
// ...and display it
if (mysqli_stmt_fetch($stmt)) {
print "A $animalName has $intelligence intelligence.\n";
} else {
print 'Sorry, no records found.';
}
// clean up statement resource
mysqli_stmt_close($stmt);
}
mysqli_close($connect);
?>
mysqli擴展提供了所有的查詢功能。
mysqli擴展也提供了面向對象的版本:
#!php
<?php
$animalName = $_POST['animalName'];
$mysqli = new mysqli('localhost', 'username', 'password', 'database');
if (!$mysqli)
exit('connection failed: ' . mysqli_connect_error());
$stmt = $mysqli->prepare("SELECT intelligence FROM animals WHERE name = ?");
if ($stmt) {
$stmt->bind_param("s", $animalName);
$stmt->execute();
$stmt->bind_result($intelligence);
if ($stmt->fetch()) {
print "A $animalName has $intelligence intelligence.\n";
} else {
print 'Sorry, no records found.';
}
$stmt->close();
}
$mysqli->close();
?>
xss攻擊一個常用的方法就是注入HTML元素執行js腳本,php中已經內置了一些防御的函數(如htmlentities或者htmlspecialchars):
#!php
<?php
function safe($value)
{
htmlentities($value, ENT_QUOTES, 'utf-8');
// other processing
return $value;
}
// retrieve $title and $message from user input
$title = $_POST['title'];
$message = $_POST['message'];
// and display them safely
print '<h1>' . safe($title) . '</h1>
<p>' . safe($message) . '</p>';
?>
如果允許用戶輸入一個URL用來調用一個圖片或者鏈接,你需要保證他不傳入javascript:或者vbscript:或data:等非http協議。
可以使用php的內置函數parse_url()函數來分割URL,然后做判斷。
設置允許信任的域:
#!php
<?php
$trustedHosts = array(
'example.com',
'another.example.com'
);
$trustedHostsCount = count($trustedHosts);
function safeURI($value)
{
$uriParts = parse_url($value);
for ($i = 0; $i < $trustedHostsCount; $i++) {
if ($uriParts['host'] === $trustedHosts[$i]) {
return $value;
}
}
$value .= ' [' . $uriParts['host'] . ']';
return $value;
}
// retrieve $uri from user input
$uri = $_POST['uri'];
// and display it safely
echo safeURI($uri);
?>
遠程執行通常是使用了php代碼執行如eval()函數,或者是調用了命令執行如exec(),passthru(),proc_open(),shell_exec(),system()或popen()。
注入php代碼:
php為開發者提供了非常多的方法可以來調用允許php腳本,我們就需要注意對用戶可控的數據進行過濾。
include()和require()函數,eval()函數,preg_replace()采用e模式調用,編寫腳本模板。
#!php
<?php
print Hello . world;
?>
上面代碼將會輸出Helloworld,php在解析的時候會檢查是否存在一個名為Hello的函數。
如果沒有找到的話,他會自己創建一個并把它的名字作為它的值,world也是一樣。
上傳文件中嵌入php代碼:
攻擊者可以上傳一個看似很普通的圖片,PDF等,但是實際上呢?
linux下可以使用如下命令插入php代碼進入圖片中:
$ echo '<?php phpinfo();?>' >> locked.gif
把代碼插入到了locked.gif圖片中。
并且此時用file命令查看文件格式仍為圖片:
$ file -i locked.giflocked.gif: image/gif
任何的圖像編輯或圖像處理的程序包括PHP的getimagesize()函數,都會認為它是一個GIF圖像。
但是當你使用cat命令查看圖片時,可以看到圖片末尾的
當把圖片的后綴改為php或者已php的方式解析時,插入的phpinfo()函數便會執行。
PHP提供了一些可以直接執行系統命令的函數,如exec()函數或者 `(反引號)。
PHP的安全模式會提供一些保護,但是也有一些方式可以繞過安全模式:
1、上傳一個Perl腳本,或者Python或Ruby等,服務器支持的環境,來執行其他語言的腳本可繞過PHP的安全模式。
2、利用系統的緩沖溢出漏洞,繞過安全模式。
下表列出了跟Shell相關的一些字符:
名稱 | 字符 | ASCII | 16進制 | URL編碼 | HTML編碼 |
---|---|---|---|---|---|
換行 | 10 | \x0a | %0a | 
 | |
感嘆號 | ! | 33 | \x21 | %21 | ! |
雙引號 | " | 34 | \x22 | %22 | "或" |
美元符號 | $ | 36 | \x24 | %24 | $ |
連接符 | & | 38 | \x26 | %26 | &或&#amp |
單引號 | ' | 39 | \x27 | %27 | ' |
左括號 | ( | 40 | \x28 | %28 | ( |
右括號 | ) | 41 | \x29 | %29 | ) |
星號 | * | 42 | \x2a | %2a | * |
連字符號 | - | 45 | \x2d | %2d | - |
分號 | ; | 59 | \x3b | %3b | ; |
左尖括號 | < | 60 | \x3c | %3c | < |
右尖括號 | > | 62 | \x3e | %3e | > |
問號 | ? | 63 | \x3f | %3f | ? |
左方括號 | [ | 91 | \x5b | %5b | [ |
反斜線 | \ | 92 | \x5c | %5c | \ |
右方括號 | ] | 93 | \x5d | %5d | ] |
插入符 | ^ | 94 | \x5e | %5e | ^ |
反引號 | ` | 96 | \x60 | %60 | ` |
左花括號 | { | 123 | \x7b | %7b | { |
管道符 | | | 124 | \x7c | %7c | | |
右花括號 | } | 125 | \x7d | %7d | } |
波浪號 | ~ | 126 | \x7e | %7e | ~ |
如下PHP腳本:
#!php
<?php
// get the word count of the requested file
$filename = $_GET['filename'];
$command = "/usr/bin/wc $filename";
$words = shell_exec($command);
print "$filename contains $words words.";
?>
用戶可以輸入如下的URL來攻擊讀取passwd文件:
wordcount.php?filename=%2Fdev%2Fnull%20%7C%20cat%20-%20%2Fetc%2Fpasswd
字符串拼接之后,將會執行 /usr/bin/wc /dev/null | cat - /etc/passwd
這條命令
如果能夠不適用命令執行函數與eval()函數,可以在php.ini中禁止:disable_functions = "eval,phpinfo"
PHP中還有一個preg_replace()函數,可能引起代碼執行漏洞。
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit ] )
在 subject 中搜索 pattern 模式的匹配項并替換為 replacement 。如果指定了 limit ,則僅替換 limit 個匹配。
如果省略 limit 或者其值為 -1,則所有的匹配項都會被替換。
特別注意:
/e 修正符使 preg_replace() 將 replacement 參數當作 PHP 代碼(在適當的逆向引用替換完之后)。
提示:要確保 replacement 構成一個合法的 PHP 代碼字符串,否則 PHP 會在報告在包含 preg_replace() 的行中出現語法解析錯誤。
#!php
<?php
function test($str)
{
//......
//......
return $str;
}
echo preg_replace("/\s*\[p hp language=""](.+?)\[\/php\]\s*/ies", 'test("\1")', $_GET["h"]);
?>
當用戶輸入
?h=[p hp]phpinfo()[/php]
經過正則匹配后, replacement 參數變為'test("phpinfo()")',
此時phpinfo僅是被當做一個字符串參數了。
但是當我們提交
?h=[p hp]{${phpinfo()}}[/php]
時,phpinfo()就會被執行。
在php中,雙引號里面如果包含有變量,php解釋器會將其替換為變量解釋后的結果;單引號中的變量不會被處理。
注意:雙引號中的函數不會被執行和替換。
在這里我們需要通過{${}}構造出了一個特殊的變量,'test("{${phpinfo()}}")',達到讓函數被執行的效果 ${phpinfo()} 會被解釋執行。