防止 ASP.NET Core 中的跨網站指令碼 (XSS)

作者:Rick Anderson

跨網站指令碼 (XSS) 是一項安全性弱點,可讓攻擊者將用戶端指令碼 (通常是 JavaScript) 放入網頁。 當其他使用者載入受影響的頁面時,攻擊者的指令碼會執行、讓攻擊者竊取 cookie 和會話權杖、透過 DOM 操作變更網頁的內容,或將瀏覽器重新導向至另一個頁面。 當應用程式接受使用者輸入並將其輸出到頁面,卻沒有對其進行驗證、編碼或將其逸出時,通常會出現 XSS 弱點。

本文主要適用於具有檢視、Razor Pages 和其他可能容易受到 XSS 攻擊之 HTML 的 Core MVC ASP.NET Core MVC。 以 HTML、XML 或 JSON 形式傳回資料的 Web API,如果用戶端應用程式未適當地清理使用者輸入,可能會觸發其用戶端應用程式中的 XSS 攻擊,視用戶端應用程式在 API 中放置多少信任而定。 例如,如果 API 接受使用者產生的內容並在 HTML 回應中傳回,攻擊者可以將惡意指令碼插入使用者在瀏覽器中轉譯回應時執行的內容。

若要防止 XSS 攻擊,Web API 應該實作輸入驗證和輸出編碼。 輸入驗證可確保使用者輸入符合預期的準則,且不包含惡意程式碼。 輸出編碼可確保 API 傳回的任何資料都經過適當清理,因此無法由使用者的瀏覽器以程式碼的形式執行。 如需詳細資訊,請參閱這個 GitHub 問題。

保護應用程式免於 XSS

在基本層級,XSS 的運作方式是欺騙應用程式將 <script> 標籤插入轉譯頁面,或將 On* 事件插入元素。 開發人員應使用下列預防步驟,以避免將 XSS 引入其應用程式中:

  1. 除非您遵循下列步驟的其餘步驟,否則請勿將不受信任的資料放入您的 HTML 輸入中。 不受信任的資料是由攻擊者控制的任何資料,例如 HTML 表單輸入、查詢字串、HTTP 標頭,甚至是來自資料庫的資料來源,因為攻擊者即使無法入侵您的應用程式,也可能可以入侵您的資料庫。

  2. 將不受信任的資料放在 HTML 元素內之前,請確認它已進行 HTML 編碼。 HTML 編碼會採用這類 < 字元,並將其變更為安全形式,例如 <

  3. 將不受信任的資料放在 HTML 屬性內之前,請確認它已進行 HTML 編碼。 HTML 屬性編碼是 HTML 編碼的超集合,會將其他字元 (例如 " 和 ") 編碼。

  4. 將不受信任的資料放入 JavaScript 之前,請將資料放在您在執行時間擷取的內容的 HTML 元素中。 如果無法這樣做,請確定資料已編碼為 JavaScript。 JavaScript 編碼會取出 JavaScript 的危險字元,並以其十六進位值取代它們,例如將 < 編碼為 \u003C

  5. 將不受信任的資料放入 URL 查詢字串之前,請確定其已編碼為 URL。

使用 Razor 進行 HTML 編碼

MVC 中使用的 Razor 引擎會自動將所有來自變數來源的輸出編碼,除非您非常努力地防止它這樣做。 每當您使用 @ 指示詞時,它會使用 HTML 屬性編碼規則。 因為 HTML 屬性編碼是 HTML 編碼的超集合,這表示您不需要擔心是否應該使用 HTML 編碼或 HTML 屬性編碼。 您必須確認您只在 HTML 內容中使用 @ ,而不是在嘗試將不受信任的輸入直接插入 JavaScript 時使用。 標籤協助程式也會將您在標記參數中使用的輸入編碼。

請檢視下列 Razor :

@{
    var untrustedInput = "<\"123\">";
}

@untrustedInput

此檢視會輸出 untrustedInput 變數的內容。 此變數包含 XSS 攻擊中使用的一些字元,亦即 <、" 和 >。 檢查來源會顯示編碼為以下內容的轉譯輸出:

&lt;&quot;123&quot;&gt;

警告

ASP.NET Core MVC 提供不會在輸出時自動編碼的 HtmlString 類別。 這不應該與不受信任的輸入搭配使用,因為這樣會暴露出 XSS 弱點。

使用 Razor 的 JavaScript 編碼

有時候您可能會想要將值插入 JavaScript,以在檢視中處理。 做法有二種。 插入值最安全的方式是將值放在標記的資料屬性中,並在您的 JavaScript 中擷取它。 例如:

@{
    var untrustedInput = "<script>alert(1)</script>";
}

<div id="injectedData"
     data-untrustedinput="@untrustedInput" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it
    // can lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or you can use createElement() to dynamically create document elements
    // This time we're using textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>

上述標記會產生下列 HTML:

<div id="injectedData"
     data-untrustedinput="&lt;script&gt;alert(1)&lt;/script&gt;" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it can
    // lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or you can use createElement() to dynamically create document elements
    // This time we're using textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>

上述程式碼會產生下列輸出:

<script>alert(1)</script>
<script>alert(1)</script>
<script>alert(1)</script>

警告

建立 DOM 元素時,請不要在 JavaScript 中串連不受信任的輸入,或針對動態產生的內容使用 document.write()

使用下列其中一種方法來防止程式碼暴露於 DOM 型 XSS 攻擊:

  • createElement() 和使用適當的方法或屬性 (例如 node.textContent=node.InnerText=) 指派屬性值。
  • document.CreateTextNode() 並將它附加在適當的 DOM 位置。
  • element.SetAttribute()
  • element[attribute]=

在程式碼中存取編碼器

HTML、JavaScript 和 URL 編碼器有兩種方式可供您的程式碼使用:

  • 透過相依性插入將它們插入。
  • 使用 System.Text.Encodings.Web 命名空間中包含的預設編碼器。

使用預設編碼器時,套用至要視為安全字元範圍的任何自訂都不會生效。 預設編碼器會使用最安全的編碼規則。

若要透過 DI 使用可設定的編碼器,建構函式應視需要採用 HtmlEncoderJavaScriptEncoderUrlEncoder 參數。 例如;

public class HomeController : Controller
{
    HtmlEncoder _htmlEncoder;
    JavaScriptEncoder _javaScriptEncoder;
    UrlEncoder _urlEncoder;

    public HomeController(HtmlEncoder htmlEncoder,
                          JavaScriptEncoder javascriptEncoder,
                          UrlEncoder urlEncoder)
    {
        _htmlEncoder = htmlEncoder;
        _javaScriptEncoder = javascriptEncoder;
        _urlEncoder = urlEncoder;
    }
}

編碼 URL 參數

如果您想要使用不受信任的輸入來建置 URL 查詢字串做為值,請使用 UrlEncoder 將值編碼。 例如,

var example = "\"Quoted Value with spaces and &\"";
var encodedValue = _urlEncoder.Encode(example);

編碼之後,encodedValue 變數會包含 %22Quoted%20Value%20with%20spaces%20and%20%26%22。 空格、引號、標點符號和其他不安全的字元會被編碼為百分號加上其十六進位的值,例如空白字元會變成 %20。

警告

請勿使用不受信任的輸入作為 URL 路徑的一部分。 不受信任的輸入一律以查詢字串值的形式傳遞。

自訂編碼器

根據預設,編碼器會使用限制為基本拉丁文 Unicode 範圍的安全清單,並將該範圍以外的所有字元編碼為其字元碼對等值。 此行為也會影響 Razor TagHelper 和 HtmlHelper 轉譯,因為它會使用編碼器來輸出您的字串。

背後的原因是要防止未知或未來的瀏覽器錯誤 (先前的瀏覽器錯誤已根據非英文字元的處理來嘗試剖析)。 如果您的網站大量使用非拉丁字元,例如中文、斯拉夫或其他字元,這可能不是您想要的行為。

您可以在 Program.cs 中自訂編碼器安全清單,以便在啟動期間納入應用程式適用的 Unicode 範圍:

例如,使用預設設定 (會使用 Razor HtmlHelper),如下所示:

<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

上述標記會以編碼的中文文字來轉譯:

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

若要擴大編碼器視為安全的字元,請將下列這一行插入 Program.cs.:

builder.Services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));

您可以在 ConfigureServices() 中自訂編碼器安全清單,以便在啟動期間納入應用程式適用的 Unicode 範圍。

例如,使用預設設定時,您可能會使用 Razor HtmlHelper,如下所示;

<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

當您檢視網頁的來源時,您會看到它已轉譯如下,且中文文字已編碼;

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

若要將編碼器視為安全的字元擴大,您要在 startup.cs 中將下列這一行插入 ConfigureServices()方法中;

services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));

本範例會擴大安全清單以包含 Unicode 範圍 CjkUnifiedIdeographs。 轉譯的輸出現在會變成

<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>

安全清單範圍指定為 Unicode 字碼圖表,而不是語言。 Unicode 標準擁有字碼圖表清單,您可以用來尋找包含您字元的圖表。 每個編碼器、Html、JavaScript 和 Url 都必須個別設定。

注意

安全清單的自訂只會影響透過 DI 來源的編碼器。 如果您透過 System.Text.Encodings.Web.*Encoder.Default 直接存取編碼器,則會使用預設值,只會使用基本拉丁文安全清單。

編碼應該在何處進行?

一般接受的做法是,編碼發生在輸出點,編碼值不應該儲存在資料庫中。 輸出點的編碼可讓您變更資料的使用方式,例如,從 HTML 變更為查詢字串值。 它也可讓您輕鬆地搜尋資料,而不需在搜尋之前將值編碼,並可讓您利用對編碼器所做的任何變更或錯誤修正。

以 XSS 預防技術進行驗證

驗證可以是限制 XSS 攻擊的有用工具。 例如,只包含 0-9 字元的數值字串不會觸發 XSS 攻擊。 在使用者輸入中接受 HTML 時,驗證會變得更加複雜。 剖析 HTML 輸入就算並非不可能,也是很困難的。 Markdown 加上去除內嵌 HTML 的剖析器,是接受內容豐富的輸入更安全的選項。 永遠不要單獨依賴驗證。 不論驗證或清理是否已執行,一律在輸出之前將不受信任的輸入編碼。