網路應用程式安全指南/跨站指令碼 (XSS)
如果 web 應用程式輸出中包含的使用者輸入沒有正確轉義,就會出現 XSS 漏洞。這種漏洞允許攻擊者向 web 應用程式輸出中注入內容。這可以用於注入虛假登入表單(向攻擊者報告輸入)或惡意 JavaScript 程式碼,這些程式碼可以竊取 cookie 和資訊,或使用使用者的許可權執行操作。XSS 漏洞分為兩大類,即*反射*(非持久)和*持久*漏洞。
反射型 XSS 漏洞僅在請求後直接在輸出中包含使用者輸入。因此,攻擊者需要使用者點選惡意連結或發出惡意 POST 請求。前者可以透過將連結作為 IFRAME 包含來完成;後者可以透過 JavaScript 完成。這兩種漏洞都需要使用者訪問惡意/受損的網站,但它們並不一定需要使用者互動。
持久型 XSS 漏洞儲存使用者輸入,並在以後的輸出中包含它(例如,在論壇中的帖子)。這意味著使用者不需要訪問惡意/受損的網站。
為了防止這種型別的攻擊
- 在將任何非常量內容包含在響應中之前對其進行轉義,儘可能靠近輸出(即在包含“echo”或“print”呼叫的行中)
- 如果不可能(例如,在構建更大的 HTML 塊時),在構建時進行轉義,並在名稱中表明變數內容是預轉義的,以及預期的上下文
- 在轉義時考慮上下文:在 HTML 中轉義文字與轉義 HTML 屬性值不同,與轉義 CSS 或 JavaScript 中的值或 HTTP 頭中的值也大不相同。
- 這意味著你可能需要針對多個上下文和/或多次進行轉義。例如,當將 HTML 片段作為 JS 常量傳遞以供稍後包含在文件中時,你需要在將常量寫入 JavaScript 原始碼時針對 HTML 中的 JS 字串進行轉義,然後在你的指令碼將片段寫入文件時再次針對 HTML 進行轉義。(見理由中的例子)
- 攻擊者不應能夠將任何內容放到不應該放置的地方,即使你認為它不可利用(例如,因為嘗試利用它會導致 JavaScript 崩潰)。
- 在文件開頭(即儘快)和/或在頭部顯式設定正確的字元集。
- 確保使用者提供的 URL 以允許的方案開頭(白名單),以避免危險方案(例如,javascript:-URL)
- 不要忘記重定向指令碼中的 URL
- 可以將 內容安全策略 用作額外的安全措施,但它本身不足以防止攻擊。
理由
在輸出位置直接轉義資料,可以更容易地檢查所有輸出是否都已轉義 - 每個用作輸出方法引數的變數,要麼必須標記為預轉義,要麼必須用相應的轉義命令包裝。
不同的上下文需要完全不同的轉義規則。一個“)”字元在 HTML 和 HTML 屬性中沒有危險含義,但在 CSS 中可以表示 URL 路徑的結束。見下面的例子,它展示了一個複雜但常見的用例,其中 HTML 和 JavaScript 一起使用,併為 XSS 創造了無數的機會。請注意,即使是錯誤的轉義,也會“意外地”阻止許多簡單的 XSS 嘗試(例如,HTML 轉義會破壞 JavaScript 字串注入所需的引號,或者換行符在注入嘗試的情況下會建立無效的 JavaScript)。不要依賴此。攻擊者可能知道一個你沒有想到的技巧。如果可以在文件結構中任何不應放置的地方放置任何內容(例如,在 JavaScript 字串文字之外),則這是一個必須修復的安全問題。它可能不可利用 - 或者你可能只是沒有看到利用它的方法。不要冒險!
未設定字元集可能會導致瀏覽器猜測。這種猜測可以被利用來傳遞一個在你的預期編碼中看起來無害的字串,但在瀏覽器假定的編碼中會被解釋為一個指令碼標籤。對於 HTML5,請在頭部部分使用<meta charset="utf-8" /> 作為第一個元素。
URL 也可能很危險。使用者提供的連結應該與方案白名單進行比較,因為 javascript 方案不是唯一危險的方案。其他方案可能會觸發可能不需要的操作。如果只允許 web 連結,則要求 URL 以“http://”或“https://”開頭。
一個 內容安全策略 可以阻止某些型別的注入。只有某些瀏覽器支援它;其他瀏覽器只是忽略它。它是一種強大的輔助防禦,可以限制安全問題的影響,但不能用作防止 XSS 的主要方法 - 防止 XSS 的主要方法是正確的轉義,這不僅可以防止 XSS,還可以確保你的頁面即使在存在不常見輸入的情況下也能正確顯示。實施 CSP 可能需要對你的程式碼進行重大更改。值得注意的是,你不能包含任何內聯 JavaScript(除非你在你的 CSP 中顯式允許內聯 JS - 這將消除 CSP 提供的大部分保護)。
包含 JS 的複雜 XSS 示例
經常被忽視的問題包括 HTML 和 JavaScript 之間的複雜互動。一種常用的構造方法類似於以下內容
<script>
var CURRENT_VALUE = 'test';
document.getElementById("valueBox").innerHTML = CURRENT_VALUE; // INSECURE CODE - DO NOT USE.
</script>
CURRENT_VALUE 的內容(在本例中為單詞test)根據例如使用者輸入或資料庫中的值由伺服器動態插入頁面原始碼中。第二行(實際將資料寫入文件的程式碼)通常是包含在檔案中的指令碼的一部分。除非在每一步都使用適當的轉義,否則有很多方法可以針對這種構造進行 XSS 攻擊。在我們的例子中,攻擊者想要執行程式碼alert(1);。
首先,如果缺少 JavaScript 的適當轉義,攻擊者只需提供適當的引號符號來終止字串,一個分號,他的程式碼,然後註釋掉剩下的程式碼行。例如,攻擊者可以提供值';alert(1);//,從而產生以下 HTML 程式碼,執行他的程式碼
<script>
var CURRENT_VALUE = '';alert(1);//';
document.getElementById("valueBox").innerHTML = CURRENT_VALUE;
</script>
請注意,即使使用像htmlspecialchars() 這樣的 HTML 轉義函式對值進行轉義,這也能正常工作,如果該函式沒有處理本例中使用的單引號。
假設攻擊者不能使用適當的引號,因為它被過濾了,他可以使用值</script><script>alert(1);</script>。在常規 JavaScript 檔案中,生成的程式碼行不會立即引起問題(儘管將其分配給 innerHTML 會引起問題),因為以下是一個完全安全的變數賦值
var CURRENT_VALUE = '</script><script>alert(1);</script>';
但是,由於這出現在內聯指令碼塊中,HTML 解析器會解釋“script-end”標籤,從而導致一個損壞的 JavaScript 程式碼片段,後面跟著一個包含攻擊者程式碼、一些文字和一個偽造的 script-end 標籤的第二個指令碼塊
<script>
var CURRENT_VALUE = '</script><script>alert(1);</script>';
document.getElementById("valueBox").innerHTML = CURRENT_VALUE;
</script>
或者,為了清晰起見,重新縮排
<script>
var CURRENT_VALUE = '
</script>
<script>alert(1);</script>
'; document.getElementById("valueBox").innerHTML = CURRENT_VALUE;
</script>
攻擊者還可以簡單地在字串末尾插入一個反斜槓來破壞 JavaScript,從而跳脫字元串末尾的引號
var CURRENT_VALUE = 'text\';
字串中的任何地方的簡單換行符也會導致語法錯誤(未終止的字串文字)。雖然這些攻擊在本例中不允許直接進行 XSS,但它們可能會破壞關鍵的安全功能,使網站無法使用(拒絕服務),或者如果另一個值可以被操縱,則允許 XSS - 在此,攻擊者向該構造的變體提供text\ 和 ;alert(1);',該變體傳遞了兩個值
var CURRENT_VALUE1 = 'text\'; var CURRENT_VALUE2 = ';alert(1);'';
由於字串結束的引號被轉義了,因此應該開始第二個字串的引號反而關閉了第一個字串,將剩餘的內容變成了 JavaScript。這使我們回到了上面的陳述:如果可以在文件結構中任何不應放置的地方放置任何內容(例如,在 JavaScript 字串文字之外),則這是一個必須修復的安全問題。它可能不可利用 - 或者你可能只是沒有看到利用它的方法。不要冒險!
這些僅僅是我們示例中第一行存在的問題。第二行直接將值作為 HTML 插入文件,從而允許 XSS。為了利用這一點,攻擊者必須由於上述問題而避免使用指令碼結束標籤,因此他使用一個帶有錯誤處理程式的非現有影像。他的輸入<img src=1 onerror=alert(1)> 導致
<script>
var CURRENT_VALUE = '<img src=1 onerror=alert(1)>';
document.getElementById("valueBox").innerHTML = CURRENT_VALUE;
</script>
innerHTML 賦值將影像標籤放入文件中,並且由於“1”不是有效的 URL,因此會執行錯誤處理程式。請注意,這不是完全有效的 HTML,因為缺少屬性周圍的引號。它仍然有效到足以工作,並且避免了由於轉義而使引號變得混亂。
在伺服器端(在將其寫入變數賦值行時)簡單地使用像htmlspecialchars() 這樣的函式對輸出值進行 HTML 轉義,可以防止其中一些攻擊,並可能使其他攻擊變得不可利用或更難利用。但是,這樣做是不正確且危險的,並將留下其他攻擊手段!
最值得注意的是,攻擊者可能會決定做你應該做的事情,併為你正確地轉義他的攻擊序列。這將留下反斜槓\ 作為唯一的特殊字元,給出像\u003Cimg src=1 onerror=alert(1)\u003E 這樣的輸入(注意任何剩餘的字元,即空格、大括號、等號和字母也可以被轉義)。這不會受到你的轉義函式的傷害,從而導致以下程式碼
<script>
var CURRENT_VALUE = '\u003Cimg src=1 onerror=alert(1)\u003E';
document.getElementById("valueBox").innerHTML = CURRENT_VALUE;
</script>
JavaScript 解析器會解釋轉義序列並將 XSS 程式碼插入你的文件中。
在這種情況下,有兩種正確的方式進行轉義
- 方法 1 - 在伺服器端進行 JS 轉義,在客戶端進行 HTML 轉義(推薦)
- 在伺服器上,使用 JavaScript 轉義值正確地(見下文)轉義。
- 在客戶端的 JavaScript 中,確保在將文字插入文件之前轉義文字,例如使用 jQuery 的 .text() setter。
- 方法 2 - 在伺服器端進行 HTML 轉義,在客戶端進行 JS 轉義(不推薦)
- 在伺服器上,首先對 HTML 進行轉義。
- 在伺服器上,然後在將值插入文件之前,正確地(見下文)使用 JavaScript 轉義值進行轉義。
方法 2 允許您將伺服器生成的自定義 HTML 傳遞到客戶端。您需要像任何其他 HTML 輸出一樣轉義 HTML(例如,使用 PHP 中的 htmlspecialchars)。然後,轉義的內容將被傳遞到客戶端,客戶端會直接將其轉儲到文件中。這意味著客戶端無法將文字用於任何非 HTML 上下文,嘗試這樣做可能會導致安全問題。正如您所看到的,轉義是按相反順序進行的:最後解釋的格式(在本例中為 HTML)首先進行轉義,然後整個字串被外部格式的轉義“包裝”。
推薦的方法是在文字準備好輸出之前保持未轉義,然後在輸出之前進行轉義(即知道上下文時)。始終如一地遵循這種方法還可以避免雙重編碼(即在文字中向用戶顯示 & 等 HTML 實體)。
如何在 HTML 中正確地對 JavaScript 進行轉義:確保像 < 這樣的字元,它們在 JavaScript 中沒有特殊含義,但在 HTML 中有特殊含義,也得到轉義。不要編寫自己的轉義例程,您很可能會遺漏一些內容。使用現有的庫。對於當前版本的 PHP,您可能需要考慮使用帶有額外標誌集的 json_encode()
...
<script>
var CURRENT_VALUE = <?php echo json_encode($text,
JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS); ?>;
$("#valueBox").text(CURRENT_VALUE);
</script>
...
即使文字包含奇怪的特殊字元,現在也能正確渲染。