跨站脚本攻击(XSS)一直是OWASP安全漏洞榜单上的常客,其核心问题在于:服务端把用户提交的数据原样输出到了HTML页面中,攻击者便可以借机注入一段恶意JavaScript,在所有访问该页面的用户浏览器中执行。C#开发者在ASP.NET Core中其实拥有相当完善的防护工具链,但很多人只知道Razor会自动编码,却不清楚哪些场景会绕过编码,最终留下安全缺口。本文从XSS原理讲起,逐层拆解ASP.NET Core中的防护手段。

理解XSS的攻击原理与三种类型
XSS的本质是数据与代码的边界被打破。当用户输入的内容未经处理就被拼进HTML,浏览器无法区分这是页面本身的脚本还是用户注入的脚本,于是照单全收地执行。比如一个留言板功能,如果直接把用户留言输出到页面,攻击者只需提交一段带有<script>标签的内容,就能让后续访问者的浏览器执行恶意代码。
XSS通常分为三类。第一类是反射型XSS,恶意脚本包含在URL参数中,服务端接收到请求后把参数直接回显到响应页面,典型场景是搜索结果页显示搜索关键词。第二类是存储型XSS,危害最大,攻击脚本被持久化保存到数据库,所有浏览相关页面的用户都会中招,比如评论区、个人签名等字段。第三类是DOM型XSS,服务端完全没有参与,是前端JavaScript在操作DOM时使用了不安全的方式,例如把location.hash直接赋值给innerHTML。
针对前两类,防护重点在服务端的输出编码;针对DOM型XSS,则需要前端配合使用安全的API。C#开发者主要控制的是服务端这一层,这也是本文的重点。
HtmlEncoder输出编码:最基础也最核心的防线
输出编码的思路很简单:把HTML中的特殊字符转换成对应的实体。比如<会被编码成<,这样浏览器就把它当作普通文本渲染,而不是标签的开始符号。ASP.NET Core提供了内置的HtmlEncoder类,可以直接注入使用。
public class CommentService
{
private readonly HtmlEncoder _htmlEncoder;
public CommentService(HtmlEncoder htmlEncoder)
{
_htmlEncoder = htmlEncoder;
}
public string Sanitize(string userContent)
{
// 将特殊字符编码为HTML实体,防止脚本被执行
return _htmlEncoder.Encode(userContent);
}
}在Controller中手动拼接HTML时,务必使用编码器处理用户数据:
public IActionResult Search(string keyword)
{
var encodedKeyword = HtmlEncoder.Default.Encode(keyword);
var html = $"<p>您搜索的关键词是:{encodedKeyword}</p>";
return Content(html, "text/html");
}需要注意的是,编码必须发生在输出阶段,而不是输入阶段。有些项目习惯在保存数据时先编码一次,这个做法有隐患:数据可能被输出到不同上下文,比如JSON、URL、JavaScript变量,HTML编码并不适用于所有场景。正确的策略是存储原始数据,输出时按目标上下文选择对应的编码器,UrlEncoder处理URL场景,JavaScriptEncoder处理脚本场景。
Razor引擎的自动编码机制与危险写法
Razor是ASP.NET Core中最常用的视图引擎,它的默认行为是安全的:@Model.Name这样的语法会自动经过HTML编码。也就是说,大多数情况下开发者不需要额外做什么,输出就是安全的。真正危险的是那些绕过编码的写法。
第一个危险写法是Html.Raw。这个方法会原样输出内容,完全跳过编码,如果传入的值包含用户输入,就等于打开了XSS的大门。除非你确定内容来源可信,或者已经做过净化处理,否则不要使用它。第二个危险写法是HtmlString或MvcHtmlString,它们同样告诉Razor这段内容不需要编码。
<!-- 安全:自动编码 --> <div>@Model.UserComment</div> <!-- 危险:跳过编码,可能触发XSS --> <div>@Html.Raw(Model.UserComment)</div> <!-- 危险:属性值中使用Raw同样有风险 --> <div data-content="@Html.Raw(Model.UserInput)">内容</div>
JavaScript上下文也需要特别注意。把用户数据嵌入到<script>块中时,HTML编码帮不上忙,因为脚本解析器不认识HTML实体。此时应使用JsonSerializer序列化,并配合JavaScriptEncoder:
<script>
// 使用Json序列化传递数据,避免字符串拼接
var userConfig = @Html.Raw(JsonSerializer.Serialize(Model.Config, jsonOptions));
</script>如果你需要在视图中允许部分HTML标签(比如富文本编辑器的内容),简单编码会让内容变成一堆实体字符串,此时需要引入HTML净化库,例如AngleSharp或HtmlSanitizer,只保留白名单内的标签和属性。
多层防御:CSP响应头与Cookie安全属性
输出编码是第一道防线,但成熟的安全方案讲究纵深防御。内容安全策略(CSP)是浏览器层面的强制约束,通过Content-Security-Policy响应头告诉浏览器只允许执行白名单内的脚本来源,即使攻击者成功注入了<script>标签,浏览器也会拒绝执行。
在ASP.NET Core中可以通过中间件统一添加CSP头:
public class CspMiddleware
{
private readonly RequestDelegate _next;
public CspMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
context.Response.Headers.Append(
"Content-Security-Policy",
"default-src 'self'; script-src 'self'; object-src 'none'");
await _next(context);
}
}
// 在Program.cs中注册
app.UseMiddleware<CspMiddleware>();Cookie安全同样关键。XSS攻击的常见目标是窃取会话Cookie,将Cookie标记为HttpOnly后,JavaScript便无法通过document.cookie读取它:
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true; // 禁止JavaScript访问
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // 仅HTTPS传输
options.Cookie.SameSite = SameSiteMode.Strict; // 防御CSRF
});除了这些,还应在表单层面对输入做格式校验,使用ASP.NET Core的数据注解(如[RegularExpression])限制字段格式;对上传文件的扩展名和MIME类型严格白名单校验,防止把脚本文件当作图片输出。输入校验不能替代输出编码,但能显著缩小攻击面。
常见漏洞场景自查与总结
结合实际项目经验,有几个高频出错点值得自查。一是搜索页直接回显关键词,反射型XSS的典型入口;二是个人资料中的昵称、签名字段直接渲染,存储型XSS的高发区;三是Html.Raw被滥用,尤其是在渲染后台配置内容时想当然认为数据可信;四是前端使用innerHTML渲染接口返回的富文本,绕过了服务端的所有防护。
总结一下防护要点:默认信任Razor的自动编码,任何绕过编码的操作都要三思;根据输出上下文选择正确的编码器;富文本场景使用净化库做白名单过滤;配置CSP响应头作为兜底防线;关键Cookie设置HttpOnly和Secure属性;对所有用户输入做服务端校验。安全没有一劳永逸的方案,把这些手段组合起来形成纵深防御,才能真正抵御XSS攻击。
C# XSS防护ASP.NET Core跨站脚本攻击修改时间:2026-09-13 19:20:54