PHP安全編碼
譯自:《Pro PHP Security》
驗證過濾使用者的輸入
即使是最普通的字母數字輸入也可能是危險的,列舉幾個容易引起安全問題的字元:
! $ ^ & * ( ) ~ [ ] \ | { } ' " ; < > ? - `
在資料庫中可能有特殊意義的字元:
' " ; \
還有一些非列印字元:
字元\x00或者說ASCII 0,NULL或FALSE
字元\x10和\x13,或者說ASCII 10和13,\n \r
字元\x1a或者說ASCII 26,表示檔案的結束
輸入錯誤的引數型別,也可能導致程式出現意想不到的錯誤。
輸入過多的引數值,可能導致溢位等錯誤。
PHP中驗證使用者的輸入
這裡特別要注意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變數中。
防止SQL隱碼攻擊
SQL隱碼攻擊是如何產生的:
1、接收一個由使用者提交的變數,假設變數為$variety:
#!php
$variety = $_POST['variety'];
2、接收的變數帶入構造一個資料庫查詢語句:
#!php
$query = "SELECT * FROM wines WHERE variety='$variety'";
3、把構造好的語句提交給MySQL伺服器查詢,MySQL返回查詢結果。
當由使用者輸入lagrein' or 1=1#時,產生的結果將會完全不同。
防止SQL隱碼攻擊的幾種方式:
檢查使用者輸入的型別,當使用者輸入的為數字時可以使用如下方式:
使用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攻擊
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
如果允許使用者輸入一個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()函式便會執行。
Shell命令執行
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()} 會被解釋執行。
相關文章
- 指定PHP編碼2020-04-04PHP
- PHP 程式碼安全2019-05-14PHP
- 前端安全編碼2019-03-10前端
- java安全編碼指南之:字串和編碼2020-09-16Java字串
- Python安全編碼指南2020-08-19Python
- PHP編碼gzdeflate與Golang解碼DEFLATE2021-09-09PHPGolang
- PHP編碼風格規範2019-08-02PHP
- 前端安全編碼規範2020-11-03前端
- python 安全編碼&程式碼審計2020-08-19Python
- Web前端安全之安全編碼原則2021-10-20Web前端
- PHP – 編碼規範 v1.02019-02-16PHP
- PHP 與 JS 的編碼問題2019-04-15PHPJS
- java安全編碼指南之:方法編寫指南2020-10-08Java
- PHP中文GBK編碼轉UTF-82019-02-16PHP
- java安全編碼指南之:Number操作2020-09-10Java
- php匯入時設定不同的編碼2021-09-11PHP
- php安全2024-06-05PHP
- java安全編碼指南之:執行緒安全規則2020-10-23Java執行緒
- [PHP 安全] pcc —— PHP 安全配置檢測工具2019-04-18PHP
- 如何寫出安全又可靠的PHP指令碼2021-09-22PHP指令碼
- java安全編碼指南之:基礎篇2020-08-25Java
- java安全編碼指南之:ThreadPool的使用2020-10-20Javathread
- 如何學習 PHP 原始碼 – 從編譯開始2019-02-16PHP原始碼編譯
- 智慧PHP程式碼編輯器:PhpStorm 2023 v2023.1.12023-05-04PHPORM
- PHP編碼開發調整執行工具PhpStorm 20222022-06-26PHPORM
- Nginx1.19 php8.0 原始碼編譯安裝2021-03-30NginxPHP原始碼編譯
- URL編碼:原理、應用與安全性2024-03-29
- java安全編碼指南之:Mutability可變性2020-09-03Java
- Java安全編碼之使用者輸入2020-08-19Java
- java安全編碼指南之:輸入校驗2020-09-21Java
- java安全編碼指南之:死鎖dead lock2020-10-01Java
- java安全編碼指南之:輸入注入injection2020-10-12Java
- java安全編碼指南之:檔案IO操作2020-10-27Java
- java安全編碼指南之:異常處理2020-09-29Java
- java安全編碼指南之:序列化Serialization2020-11-01Java
- java安全編碼指南之:堆汙染Heap pollution2020-09-18Java
- 編碼加密(小迪網路安全筆記~2024-12-04加密筆記
- [PHP 安全] OWASP 維護的 PHP 安全配置速查表2019-04-10PHP