在 ASP.NET Core 中防止跨站脚本 (XSS)
作者:Rick Anderson
跨站点脚本 (XSS) 是一种安全漏洞,它使网络攻击者能够将客户端脚本(通常是 JavaScript)放入网页中。 当其他用户加载受影响的页面时,网络攻击者的脚本就会运行,从而使网络攻击者能够窃取 cookie 和会话令牌、通过 DOM 操作更改网页内容,或将浏览器重定向到另一个页面。 当应用程序接收用户输入并将它输出到页面而不进行验证、编码或转义时,通常会出现 XSS 漏洞。
本文主要适用于 ASP.NET Core MVC,其中包含视图、Razor页面和其他返回可能易受 XSS 攻击的 HTML 的应用。 如果以 HTML、XML 或 JSON 形式返回数据的 Web API 没有正确清理用户输入,则可能会在其客户端应用中触发 XSS 攻击,这取决于客户端应用对 API 的信任程度。 例如,如果 API 接受用户生成的内容,并在 HTML 响应中返回内容,则网络攻击者可能会将恶意脚本注入到在用户浏览器中呈现响应时执行的内容中。
为了防止 XSS 攻击,Web API 应实现输入验证和输出编码。 输入验证可确保用户输入符合预期条件,并且不包含恶意代码。 输出编码可确保正确清理 API 返回的任何数据,以便用户浏览器无法将其作为代码执行。 有关详细信息,请参阅此 GitHub 问题。
针对 XSS 保护应用程序
在基本级别上,XSS 的工作原理是诱使应用程序将 ";
}
data-untrustedinput="@untrustedInput" />
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);
上面的 标记生成以下 HTML:
data-untrustedinput="<script>alert(1)</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);
上面的代码生成以下输出:
警告
请勿在 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、JavaScriptEncoder 和 UrlEncoder 参数。 例如;
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 会阻碍基于非英语字符处理的分析)。 如果你的网站大量使用非拉丁字符(如中文、西里尔文或其他字符),这可能不是你所希望的行为。
在 Program.cs 中,可以自定义编码器安全列表,以在启动期间添加适用于应用的 Unicode 范围:
例如,使用类似于以下内容的 Razor HtmlHelper 使用默认配置:
This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")
上述标记以中文文本编码呈现:
This link text is in Chinese: 汉语/漢語
若要扩大被编码器视为安全的字符范围,可在 Program.cs 中插入以下行:
builder.Services.AddSingleton
HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
UnicodeRanges.CjkUnifiedIdeographs }));
在 ConfigureServices() 中,你可以自定义编码器安全列表,以在启动过程中包含适用于你的应用程序的 Unicode 范围。
例如,使用默认配置时,可以使用 Razor HtmlHelper,如下所示;
This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")
查看网页的源时,你会看到它按如下进行呈现(中文文本已进行编码);
This link text is in Chinese: 汉语/漢語
若要扩大被编码器视为安全的字符范围,可在 startup.cs 中将以下行插入到 ConfigureServices() 方法中;
services.AddSingleton
HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
UnicodeRanges.CjkUnifiedIdeographs }));
此示例将安全列表扩大到包含 Unicode 范围 CjkUnifiedIdeographs。 呈现的输出现在会变为
This link text is in Chinese: 汉语/漢語
安全列表范围指定为 Unicode 代码图表,而不是语言。 Unicode 标准包含可用于查找包含你的字符的图表的代码图表列表。 每个编码器、Html、JavaScript 和 Url 都必须单独配置。
注意
安全列表的自定义仅影响来源为 DI 的编码器。 如果直接通过 System.Text.Encodings.Web.*Encoder.Default 访问编码器,则会使用默认的仅限基本拉丁语安全列表。
编码应在何处进行?
可接受的一般做法是在输出时进行编码,并且编码值绝不应存储在数据库中。 通过在输出时进行编码,可更改数据的使用,例如从 HTML 到查询字符串值。 这还使你可轻松搜索你的数据,而无需在搜索之前对值进行编码,并使你可利用对编码器进行的任何更改或 bug 修复。
将验证作为 XSS 防护方法
验证可能是限制 XSS 攻击的有用工具。 例如,仅包含字符 0-9 的数字字符串不会触发 XSS 攻击。 在用户输入中接受 HTML 时,验证会变得更加复杂。 分析 HTML 输入即使不是不可行,也是困难重重。 Markdown(与去除嵌入式 HTML 的分析器结合使用)是接受丰富输入的更安全选项。 切勿仅依赖于验证。 请在输出之前始终对不受信任的输入进行编码,无论执行了哪种验证或清理。