PHP安全編碼

wyzsk發表於2020-08-19
作者: 瞌睡龍 · 2013/06/03 18:56

譯自:《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 &#10
感嘆號 ! 33 \x21 %21 &#33
雙引號 " 34 \x22 %22 &#34或&quot
美元符號 $ 36 \x24 %24 &#36
連線符 & 38 \x26 %26 &#38或&#amp
單引號 ' 39 \x27 %27 &#39
左括號 ( 40 \x28 %28 &#40
右括號 ) 41 \x29 %29 &#41
星號 * 42 \x2a %2a &#42
連字元號 - 45 \x2d %2d &#45
分號 ; 59 \x3b %3b &#59
左尖括號 < 60 \x3c %3c &#60
右尖括號 > 62 \x3e %3e &#62
問號 ? 63 \x3f %3f &#63
左方括號 [ 91 \x5b %5b &#91
反斜線 \ 92 \x5c %5c &#92
右方括號 ] 93 \x5d %5d &#93
插入符 ^ 94 \x5e %5e &#94
反引號 ` 96 \x60 %60 &#96
左花括號 { 123 \x7b %7b &#123
管道符 | 124 \x7c %7c &#124
右花括號 } 125 \x7d %7d &#125
波浪號 ~ 126 \x7e %7e &#126

如下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()} 會被解釋執行。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章