在 ASP.NET Core 中 (XSS) 跨 ASP.NET 脚本

作者:Rick Anderson

XSS (跨站点脚本) 是一个安全漏洞,攻击者可以通过此漏洞将客户端脚本 (JavaScript) 网页。 当其他用户加载受影响的页面时,攻击者的脚本将运行,从而使攻击者能够窃取和会话令牌,通过 DOM 操作更改网页的内容,或将浏览器重定向到另一 cookie 个页面。 当应用程序接受用户输入并输出到页面而不验证、编码或转义它时,通常会发生 XSS 漏洞。

保护应用程序免受 XSS 攻击

在基本级别,XSS 的工作方式是欺骗应用程序将标记插入呈现的页面,或将事件插入 <script> On* 元素。 开发人员应使用以下预防步骤,以避免将 XSS 引入其应用程序。

  1. 切勿将不受信任的数据放入 HTML 输入中,除非执行以下步骤。 不受信任的数据是攻击者可能控制的任何数据、HTML 表单输入、查询字符串、HTTP 标头,甚至来自数据库的数据,因为攻击者即使无法破坏应用程序,也可能破坏数据库。

  2. 在 HTML 元素中放置不受信任的数据之前,请确保它是 HTML 编码的。 HTML 编码采用 等字符 < ,并将它们更改为安全形式,如 & lt;

  3. 在将不受信任的数据放入 HTML 属性之前,请确保它是 HTML 编码的。 HTML 属性编码是 HTML 编码的超集,并编码其他字符,如 " 和 '。

  4. 将不受信任的数据放入 JavaScript 之前,将数据放在在运行时检索其内容的 HTML 元素中。 如果无法这样做,请确保数据已进行 JavaScript 编码。 JavaScript 编码会为 JavaScript 使用危险字符,并将其替换为其十六进制,例如,将 < 编码为 \u003C

  5. 将不受信任的数据置于 URL 查询字符串之前,请确保其 URL 已编码。

HTML 编码使用 Razor

RazorMVC 中使用的引擎会自动对源自变量的所有输出进行编码,除非您确实很难避免这样做。 使用指令时,它将使用 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 漏洞。

JavaScript 编码使用 Razor

有时可能需要将值插入 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>

警告

不要 连接 JavaScript 中不受 信任的输入来创建 DOM 元素或 document.write() 在动态生成的内容上使用。

使用以下方法之一来防止代码向基于 DOM 的 XSS 公开:

  • createElement() 和 使用适当的方法或属性(如 或 )分配 node.textContent= 属性值 node.InnerText=
  • document.CreateTextNode() 并将其追加到相应的 DOM 位置。
  • element.SetAttribute()
  • element[attribute]=

在代码中访问编码器

HTML、JavaScript 和 URL 编码器可通过两种方式提供给代码,可以通过依赖项注入注入它们,或者可以使用 命名空间中包含的默认编码 System.Text.Encodings.Web 器。 如果使用默认编码器,则应用于要视为安全的字符范围的任何内容将不会生效 - 默认编码器使用尽可能安全的编码规则。

若要通过 DI 使用可配置的编码器,构造函数应采用 相应的 HtmlEncoder、JavaScriptEncoderUrlEncoder 参数。 例如,

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 呈现,因为它将使用编码器输出字符串。

这样做背后的原因是防止未知或将来的浏览器 bug (以前的浏览器 bug 已根据对非英语字符的处理而) 。 如果网站大量使用非拉丁字符(如中文、西里尔文或其他语言字符)可能并不是你想要的行为。

在中,你可以自定义编码器安全列表,以包含在启动过程中适用于你的应用程序的 Unicode 范围 ConfigureServices()

例如,使用默认配置时,可以使用 HtmlHelper, Razor 如下所示:

<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>

若要放宽编码器被视为安全字符,请将以下行插入到 ConfigureServices() 中的方法 startup.cs ;

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 到查询字符串值。 它还使您能够轻松搜索您的数据,而无需在搜索之前对值进行编码,还可以利用对编码器进行的任何更改或 bug 修复。

验证为 XSS 保护方法

验证是限制 XSS 攻击的有用工具。 例如,仅包含字符0-9 的数字字符串将不会触发 XSS 攻击。 在用户输入中接受 HTML 时,验证变得更加复杂。 分析 HTML 输入很困难,如果不可能。 Markdown 与去除嵌入 HTML 的分析器一起是接受丰富输入的更安全选项。 切勿仅依赖验证。 始终在输出之前对不受信任的输入进行编码,无论执行了什么验证或验证。