10 個 PHP 常見安全問題(範例講解)

2020-07-16 10:05:49
相對於其他幾種語言來說, PHP 在 web 建站方面有更大的優勢,即使是新手,也能很容易搭建一個網站出來。但這種優勢也容易帶來一些負面影響,因為很多的 PHP 教學沒有涉及到安全方面的知識。

此貼文分為幾部分,每部分會涵蓋不同的安全威脅和應對策略。但是,這並不是說你做到這幾點以後,就一定能避免你的網站出現任何問題。如果你想提高你的網站安全性的話,你應該繼續通過閱讀書籍或者文章,來研究如何提高你的網站安全性

出於演示需要,程式碼可能不是很完美。日常開發過程中,很多程式碼都包含在了框架跟各種庫裡面。作為一個後台開發,你不僅要熟練基本的 CURD,更要知道如何保護你的資料。

1. SQL 注入

我賭一包辣條,你肯定會看到這裡。 SQL 注入是對您網站最大的威脅之一,如果您的資料庫受到別人的 SQL 注入的攻擊的話,別人可以轉出你的資料庫,也許還會產生更嚴重的後果。

網站要從資料庫中獲取動態資料,就必須執行 SQL 語句,舉例如下:

<?php
$username = $_GET['username'];
$query = "SELECT * FROM users WHERE username = '$username'";

攻擊者控制通過 GET 和 POST 傳送的查詢(或者例如 UA 的一些其他查詢)。一般情況下,你希望查詢戶名為「 peter 」的使用者產生的 SQL 語句如下:

SELECT * FROM users WHERE username = 'peter'

但是,攻擊者傳送了特定的使用者名稱引數,例如:' OR '1'='1

這就會導致 SQL 語句變成這樣:

SELECT * FROM users WHERE username = 'peter' OR '1' = '1'

這樣,他就能在不需要密碼的情況下匯出你的整個使用者表的資料了。

那麼,我們如何防止這類事故的發生呢?主流的解決方法有兩種。跳脫使用者輸入的資料或者使用封裝好的語句。跳脫的方法是封裝好一個函數,用來對使用者提交的資料進行過濾,去掉有害的標籤。但是,我不太推薦使用這個方法,因為比較容易忘記在每個地方都做此處理。

下面,我來介紹如何使用 PDO 執行封裝好的語句( mysqi 也一樣):

$username = $_GET['username'];
$query = $pdo->prepare('SELECT * FROM users WHERE username = :username');
$query->execute(['username' => $username]);
$data = $query->fetch();

動態資料的每個部分都以:做字首。然後將所有引數作為陣列傳遞給執行函數,看起來就像 PDO 為你跳脫了有害資料一樣。

幾乎所有的資料庫驅動程式都支援封裝好的語句,沒有理由不使用它們!養成使用他們的習慣,以後就不會忘記了。

你也可以參考 phpdelusions 中的一篇關於動態構建 SQL 查詢時處理安全問題的文章。連結:

https://phpdelusions.net/pdo/sql_injection_example

2. XSS

XSS 又叫 CSS (Cross Site Script) ,跨站指令碼攻擊。它指的是惡意攻擊者往 Web 頁面裡插入惡意 html 程式碼,當使用者瀏覽該頁之時,嵌入其中 Web 裡面的 html 程式碼會被執行,從而達到惡意攻擊使用者的特殊目的。

下面以一個搜尋頁面為例子:

<body>
<?php
$searchQuery = $_GET['q'];
/* some search magic here */
?>
<h1>You searched for: <?php echo $searchQuery; ?></h1>
<p>We found: Absolutely nothing because this is a demo</p>
</body>

因為我們把使用者的內容直接列印出來,不經過任何過濾,非法使用者可以拼接 URL:

search.php?q=%3Cscript%3Ealert(1)%3B%3C%2Fscript%3E

PHP 渲染出來的內容如下,可以看到 Javascript 程式碼會被直接執行:

<body>
<h1>You searched for: <script>alert(1);</script></h1>
<p>We found: Absolutely nothing because this is a demo</p>
</body>

問:JS 程式碼被執行有什麼大不了的?

Javascript 可以:

● 偷走你使用者瀏覽器裡的 Cookie;

● 通過瀏覽器的記住密碼功能獲取到你的站點登入賬號和密碼;

● 盜取使用者的機密資訊;

● 你的使用者在站點上能做到的事情,有了 JS 許可權執行許可權就都能做,也就是說 A 使用者可以模擬成為任何使用者;

● 在你的網頁中嵌入惡意程式碼;

...

問:如何防範此問題呢?

好訊息是比較先進的瀏覽器現在已經具備了一些基礎的 XSS 防範功能,不過請不要依賴與此。

正確的做法是堅決不要相信使用者的任何輸入,並過濾掉輸入中的所有特殊字元。這樣就能消滅絕大部分的 XSS 攻擊:

<?php
$searchQuery = htmlentities($searchQuery, ENT_QUOTES);

或者你可以使用模板引擎 Twig ,一般的模板引擎都會預設為輸出加上 htmlentities 防範。

如果你保持了使用者的輸入內容,在輸出時也要特別注意,在以下的例子中,我們允許使用者填寫自己的部落格連結:

<body>
  <a href="<?php echo $homepageUrl; ?>">Visit Users homepage</a>
</body>

以上程式碼可能第一眼看不出來有問題,但是假設使用者填入以下內容:

#" onclick="alert(1)

會被渲染為:

<body>
  <a href="#" onclick="alert(1)">Visit Users homepage</a>
</body>

永遠永遠不要相信使用者輸入的資料,或者,永遠都假設使用者的內容是有攻擊性的,態度端正了,然後小心地處理好每一次的使用者輸入和輸出。

另一個控制 XSS 攻擊的方法是提供一個 CSP Meta 標籤,或者檔頭資訊,更多詳情請見:

https://phpdelusions.net/pdo/sql_injection_example

另外設定 Cookie 時,如果無需 JS 讀取的話,請必須設定為 "HTTP ONLY"。這個設定可以令 JavaScript 無法讀取 PHP 端種的 Cookie。

3. XSRF/CSRF

CSRF 是跨站請求偽造的縮寫,它是攻擊者通過一些技術手段欺騙使用者去存取曾經認證過的網站並執行一些操作。

雖然此處展示的例子是 GET 請求,但只是相較於 POST 更容易理解,並非防護手段,兩者都不是私密的 Cookies 或者多步表單。

假如你有一個允許使用者刪除賬戶的頁面,如下所示:

<?php
//delete-account.php
$confirm = $_GET['confirm'];
if($confirm === 'yes') {
  //goodbye
}

攻擊者可以在他的站點上構建一個觸發這個 URL 的表單(同樣適用於 POST 的表單),或者將 URL 載入為圖片誘惑使用者點選:

<img src="https://example.com/delete-account.php?confirm=yes" />

使用者一旦觸發,就會執行刪除賬戶的指令,眨眼你的賬戶就消失了。

防禦這樣的攻擊比防禦 XSS 與 SQL 注入更複雜一些。

最常用的防禦方法是生成一個 CSRF 令牌加密安全字串,一般稱其為 Token,並將 Token 儲存於 Cookie 或者 Session 中。

每次你在網頁構造表單時,將 Token 令牌放在表單中的隱藏欄位,表單請求伺服器以後會根據使用者的 Cookie 或者 Session 裡的 Token 令牌比對,校驗成功才給予通過。

由於攻擊者無法知道 Token 令牌的內容(每個表單的 Token 令牌都是隨機的),因此無法冒充使用者。

<?php /* 你嵌入表單的頁面 */ ?>
<form action="/delete-account.php" method="post">
  <input type="hidden" name="csrf" value="<?php echo $_SESSION['csrf']; ?>">
  <input type="hidden" name="confirm" value="yes" />
  <input type="submit" value="Delete my account" />
</form>
## 
<?php
//delete-account.php
$confirm = $_POST['confirm'];
$csrf = $_POST['csrf'];
$knownGoodToken = $_SESSION['csrf'];
if($csrf !== $knownGoodToken) {
  die('Invalid request');
}
if($confirm === 'yes') {
  //goodbye
}

請注意,這是個非常簡單的範例,你可以加入更多的程式碼。如果你使用的是像 Symfony 這樣的 PHP 框架,那麼自帶了 CSRF 令牌的功能。

你還可以檢視關於 OWASP 更詳細的問題和更多防禦機制的文章:

https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md

4. LFI

LFI (本地檔案包含) 是一個使用者未經驗證從磁碟讀取檔案的漏洞。

我經常遇到程式設計不規範的路由程式碼範例,它們不驗證過濾使用者的輸入。我們用以下檔案為例,將它要渲染的模板檔案用 GET 請求載入。

<body>
<?php
  $page = $_GET['page'];
  if(!$page) {
    $page = 'main.php';
  }
  include($page);
?>
</body>

由於 Include 可以載入任何檔案,不僅僅是 PHP,攻擊者可以將系統上的任何檔案作為包含目標傳遞。

index.php?page=../../etc/passwd

這將導致 /etc/passwd 檔案被讀取並展示在瀏覽器上。

要防禦此類攻擊,你必須仔細考慮允許使用者輸入的型別,並刪除可能有害的字元,如輸入字元中的 「.」 「/」 「」。

如果你真的想使用像這樣的路由系統(我不建議以任何方式),你可以自動附加 PHP 擴充套件,刪除任何非 [a-zA-Z0-9-_] 的字元,並指定從專用的模板資料夾中載入,以免被包含任何非模板檔案。

我在不同的開發文件中,多次看到造成此類漏洞的 PHP 程式碼。從一開始就要有清晰的設計思路,允許所需要包含的檔案型別,並刪除掉多餘的內容。你還可以構造要讀取檔案的絕對路徑,並驗證檔案是否存在來作為保護,而不是任何位置都給予讀取。

5. 不充分的密碼雜湊

大部分的 Web 應用需要儲存使用者的認證資訊。如果密碼雜湊做的足夠好,在你的網站被攻破時,即可保護使用者的密碼不被非法讀取。

首先,最不應該做的事情,就是把使用者密碼明文儲存起來。大部分的使用者會在多個網站上使用同一個密碼,這是不可改變的事實。當你的網站被攻破,意味著使用者的其他網站的賬號也被攻破了。

其次,你不應該使用簡單的雜湊演算法,事實上所有沒有專門為密碼雜湊優化的演算法都不應使用。雜湊演算法如 MD5 或者 SHA 設計初衷就是執行起來非常快。這不是你需要的,密碼雜湊的終極目標就是讓駭客花費無窮盡的時間和精力都無法破解出來密碼。

另外一個比較重要的點是你應該為密碼雜湊加鹽(Salt),加鹽處理避免了兩個同樣的密碼會產生同樣雜湊的問題。

以下使用 MD5 來做例子,所以請千萬不要使用 MD5 來雜湊你的密碼, MD5 是不安全的。

假如我們的使用者 user1 和 user315 都有相同的密碼 ilovecats123,這個密碼雖然看起來是強密碼,有字母有數位,但是在資料庫裡,兩個使用者的密碼雜湊資料將會是相同的:5e2b4d823db9d044ecd5e084b6d33ea5 。

如果一個如果駭客拿下了你的網站,獲取到了這些雜湊資料,他將不需要去暴力破解使用者 user315 的密碼。我們要盡量讓他花大精力來破解你的密碼,所以我們對資料進行加鹽處理:

<?php
//warning: !!這是一個很不安全的密碼雜湊例子,請不要使用!!
$password = 'cat123';
$salt = random_bytes(20);
$hash = md5($password . $salt);

最後在儲存你的唯一密碼雜湊資料時,請不要忘記連 $salt 也已經儲存,否則你將無法驗證使用者。

在當下,最好的密碼雜湊選項是 bcrypt,這是專門為雜湊密碼而設計的雜湊演算法,同時這套雜湊演算法裡還允許你設定一些引數來加大破解的難度。

新版的 PHP 中也自帶了安全的密碼雜湊函數 password_hash ,此函數已經包含了加鹽處理。對應的密碼驗證函數為 password_verify 用來檢測密碼是否正確。password_verify 還可有效防止 時序攻擊.

以下是使用的例子:

<?php
//user signup
$password = $_POST['password'];
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
//login
$password = $_POST['password'];
$hash = '1234'; //load this value from your db
if(password_verify($password, $hash)) {
  echo 'Password is valid!';
} else {
  echo 'Invalid password.';
}

需要澄清的一點是:密碼雜湊並不是密碼加密。雜湊(Hash)是將目標文字轉換成具有相同長度的、不可逆的雜湊字串(或叫做訊息摘要),而加密(Encrypt)是將目標文字轉換成具有不同長度的、可逆的密文。顯然他們之間最大的區別是可逆性,在儲存密碼時,我們要的就是雜湊這種不可逆的屬性。

6. 中間人攻擊

MITM (中間人) 攻擊不是針對伺服器直接攻擊,而是針對使用者進行,攻擊者作為中間人欺騙伺服器他是使用者,欺騙使用者他是伺服器,從而來攔截使用者與網站的流量,並從中注入惡意內容或者讀取私密資訊,通常發生在公共 WiFi 網路中,也有可能發生在其他流量通過的地方,例如 ISP 運營商。

對此的唯一防禦是使用 HTTPS,使用 HTTPS 可以將你的連線加密,並且無法讀取或者篡改流量。你可以從 Let's Encrypt 獲取免費的 SSL 證書,或從其他供應商處購買,這裡不詳細介紹如何正確設定 WEB 伺服器,因為這與應用程式安全性無關,且在很大程度上取決於你的設定。

你還可以採取一些措施使 HTTPS 更安全,在 WEB 伺服器設定加上 Strict-Transport-Security 標示頭,此頭部資訊告訴瀏覽器,你的網站始終通過 HTTPS 存取,如果未通過 HTTPS 將返回錯誤報告提示瀏覽器不應顯示該頁面。

然而,這裡有個明顯的問題,如果瀏覽器之前從未存取過你的網站,則無法知道你使用此標示頭,這時候就需要用到 Hstspreload。

可以在此註冊你的網站:

https://hstspreload.org/

你在此處提交的所有網站都將被標記為僅 HTTPS,並寫死到 Google Chrome、FireFox、Opera、Safari、IE11 和 Edge 的原始碼中。

你還可以在 DNS 設定中新增 Certification Authority Authorization (CAA) record ,可以僅允許一個證書頒發機構(例如: Let's encrypt)發布你的域名證書,這進一步提高了使用者的安全性。

7. 命令注入

這可能是伺服器遇到的最嚴重的攻擊,命令注入的目標是欺騙伺服器執行任意 Shell 命令

你如果使用 shell_exec 或是 exec 函數。讓我們做一個小例子,允許使用者簡單的從伺服器 Ping 不同的主機。

<?php
$targetIp = $_GET['ip'];
$output = shell_exec("ping -c 5 $targetIp");

輸出將包括對目標主機 Ping 5 次。除非採用 sh 命令執行 Shell 指令碼,否則攻擊者可以執行想要的任何操作。

ping.php?ip=8.8.8.8;ls -l /etc

Shell 將執行 Ping 和由攻擊者拼接的第二個命令,這顯然是非常危險的。

感謝 PHP 提供了一個函數來跳脫 Shell 引數。

escapeshellarg 跳脫使用者的輸入並將其封裝成單引號。

<?php
$targetIp = escapeshellarg($_GET['ip']);
$output = shell_exec("ping -c 5 $targetIp");

現在你的命令應該是相當安全的,就個人而言,我仍然避免使用 PHP 呼叫外部命令,但這完全取決於你自己的喜好。

另外,我建議進一步驗證使用者輸入是否符合你期望的形式。

8. XXE

XXE (XML 外部實體) 是一種應用程式使用設定不正確的 XML 解析器解析外部 XML 時,導致的本地檔案包含攻擊,甚至可以遠端程式碼執行。

XML 有一個鮮為人知的特性,它允許文件作者將遠端和本地檔案作為實體包含在其 XML 檔案中。

<?xml version="1.0" encoding="ISO-8859-1"?>
 <!DOCTYPE foo [
   <!ELEMENT foo ANY >
   <!ENTITY passwd SYSTEM "file:///etc/passwd" >]>
   <foo>&passwd;</foo>

就像這樣, /etc/passwd 檔案內容被轉儲到 XML 檔案中。

如果你使用 libxml 可以呼叫 libxml_disable_entity_loader 來保護自己免受此類攻擊。使用前請仔細檢查 XML 庫的預設設定,以確保設定成功。

9. 在生產環境中不正確的錯誤報告暴露敏感資料

如果你不小心,可能會在生產環境中因為不正確的錯誤報告洩露了敏感資訊,例如:資料夾結構、資料庫結構、連線資訊與使用者資訊。

27548e12c07caaffb1bbc9c3c9eeff8.png

你是不希望使用者看到這個的吧?

一般根據你使用的框架或者 CMS ,設定方法會有不同的變化。通常框架具有允許你將站點更改為某種生產環境的設定。這樣會將所有使用者可見的錯誤訊息重定向到紀錄檔檔案中,並向使用者顯示非描述性的 500 錯誤,同時允許你根據錯誤程式碼檢查。

但是你應該根據你的 PHP 環境設定: error_reportingdisplay_errors.

10. 登入限制

像登入這樣的敏感表單應該有一個嚴格的速率限制,以防止暴力攻擊。儲存每個使用者在過去幾分鐘內失敗的登入嘗試次數,如果該速率超過你定義的閾值,則拒絕進一步登入嘗試,直到冷卻期結束。還可通過電子郵件通知使用者登入失敗,以便他們知道自己的賬戶被成為目標。

一些其他補充

● 不要信任從使用者傳遞給你的物件 ID ,始終驗證使用者對請求物件的存取許可權

● 伺服器與使用的庫時刻保持最新

● 訂閱關注安全相關的部落格,了解最新的解決方案

● 從不在紀錄檔中儲存使用者的密碼

● 不要將整個程式碼庫儲存在 WEB 根目錄中

● 永遠不要在 WEB 根目錄建立 Git 儲存庫,除非你希望洩露整個程式碼庫

● 始終假設使用者的輸入是不安全的

● 設定系統禁止可疑行為的 IP 顯示,例如:工具對 URL 隨機掃描、爬蟲

● 不要過分信任第三方程式碼是安全的

● 不要用 Composer 直接從 Github 獲取程式碼

● 如果不希望站點被第三方跨域 iframe,請設定反 iframe 標示頭

● 含糊是不安全的

● 如果你是缺乏實踐經驗的運營商或合作開發人員,請確保盡可能時常檢查程式碼

● 當你不了解安全功能應該如何工作,或者為什麼會安裝,請詢問知道的人,不要忽視它

● 永遠不要自己寫加密方式,這可能是個壞的方法

● 如果你沒有足夠的熵,請正確播種你的偽亂數生成並捨棄

● 如果在網際網路上不安全,並有可能被竊取資訊,請為這種情況做好準備並制定事件響應計劃

● 禁用 WEB 根目錄列表顯示,很多 WEB 伺服器設定預設都會列出目錄內容,這可能導致資料洩露

● 用戶端驗證是不夠的,需要再次驗證 PHP 中的所有內容

● 不惜一切代價避免反序列化使用者內容,這可能導致遠端程式碼執行,有關此問題的詳細資訊,請參閱此文章:

https://paragonie.com/blog/2016/04/securely-implementing-de-serialization-in-php

小貼士

我不是一個安全專家,恐無法做到事無巨細。儘管編寫安全軟體是一個非常痛苦的過程,但還是可以通過遵循一些基本規則,編寫合理安全的應用程式。其實,很多框架在這方面也幫我們做了很多工作。

在問題發生之前,安全性問題並不像語法錯誤等可以在開發階段追蹤到。因此,在編寫程式碼的過程中,應該時刻有規避安全風險的意識。如果你迫於業務需求的壓力而不得不暫時忽略一些安全防範的工作,我想你有必要事先告知大家這樣做的潛在風險。

如果你從這篇文章有所收益,也請把它分享給你的朋友們把,讓我們共建安全網站。

以上就是10 個 PHP 常見安全問題(範例講解)的詳細內容,更多請關注TW511.COM其它相關文章!