如果应用程序遭受了XSS(跨站脚本攻击),那么很不幸,安全防护出现了漏洞。XSS攻击涉及到API保存包含恶意HTML或JavaScript的用户提交数据,并且可能暴露另一个端点,该端点将数据原样返回。用户提交的脚本随后被返回到另一个用户的浏览器中,并以某种方式造成破坏——可能是篡改服务,从页面上抓取私有数据并通过线路发送,或者他们能想到的任何其他恶劣行为。
确实,大多数现代浏览器和前端框架通常会有安全机制来防止渲染不受信任的HTML(例如,CSP和Angular的DomSanitizer),但这并不意味着应该将安全责任完全委托给前端;API在这方面也扮演着角色,并且应该得到保护。
ASP.NET是否为处理了这个问题?它曾经(在某种程度上)是这样做的,所以原谅这样想。正如OWASP网站上所描述的,ASP.NET曾经为Web Forms(还记得那些吗!)和MVC项目默认启用了请求验证功能,但这从未支持Web API项目。实在找不到为什么,所以如果知道,请在评论中告诉。可悲的事实是,现在绝大多数ASP.NET应用程序很可能是API,所以遗憾的是请求验证不再为所用。在预防XSS方面,只能靠自己了。
那么,现在该怎么办呢?基本上有两种方法可以处理XSS;要么对输入进行清理(或拒绝),要么对输出进行编码。
一个非常值得信赖的清理包是HtmlSanitizer,它甚至在OWASP网站上有推荐。基本用法如下:
var sanitizer = new HtmlSanitizer();
sanitizer.AllowedAttributes.Add("class");
var sanitized = sanitizer.Sanitize(html);
清理器可以配置为支持属性和标签的白名单,然后被调用以从源字符串中剥离不在白名单中的属性和标签。虽然这个库是一个几乎被一致推荐的包,但开箱即用意味着需要逐个字段进行清理。
不是在开玩笑吧?遗憾的是,不是。可以采取一些方法让清理器在每个请求上运行,但据所知,ASP.NET或该包本身并没有内置支持。会给展示一些示例,但可以下载并运行整个示例项目,方法是从GitHub克隆它。
JSON转换器:一种(尽管天真)的方法是使用自定义JSON转换器来清理进出API的内容。要做到这一点,首先创建一个新类(例如:AntiXssConverter),内容如下:
public class AntiXssConverter : JsonConverter
{
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var sanitizer = new HtmlSanitizer();
var raw = reader.GetString();
var sanitized = sanitizer.Sanitize(raw);
if (raw == sanitized)
return sanitized;
throw new BadRequestException("XSS injection detected.");
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
}
请注意,这个转换器基于.NET Core 3.1及以上版本默认提供的System.Text.Json序列化器。如果使用的是Newtonsoft.Json转换器,原理仍然相同,但实现可能会略有不同。
本质上,在这里提供的策略是在JSON字符串反序列化时读取字符串值(例如:在模型绑定期间),以及在对象序列化时写入字符串值(例如:当控制器返回响应时)。在这个特定的例子中,使用了前面介绍的HtmlSanitizer包来清理每个字符串,如果清理后的字符串与原始字符串不同,抛出一个自定义的BadRequestException,拒绝请求,HTTP 400(由自定义异常处理中间件处理)。另一种可能采取的方法是简单地清理字符串并允许请求,或者保存字符串原样并在序列化时在Write方法中进行清理(或编码)。
在Startup.cs中,需要在ConfigureServices(...)中注册转换器,如下所示:
services.AddControllers().AddJsonOptions(options=>
{
options.JsonSerializerOptions.Converters.Add(new AntiXssConverter());
});
这种方法适用于简单对象、嵌套对象和集合。然而,它有两个相当严重的缺陷:
ASP.NET中间件:一种更有效、更通用的方法可能是通过中间件来清理和验证整个请求正文。这样做的优点是:
要实现这一点,创建一个名为AntiXssMiddleware的新类,内容如下:
public class AntiXssMiddleware
{
private readonly RequestDelegate _next;
public AntiXssMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
// enable buffering so that the request can be read by the model binders next
httpContext.Request.EnableBuffering();
// leaveOpen: true to leave the stream open after disposing,
// so it can be read by the model binders
using (var streamReader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, leaveOpen: true))
{
var raw = await streamReader.ReadToEndAsync();
var sanitizer = new HtmlSanitizer();
var sanitized = sanitizer.Sanitize(raw);
if (raw != sanitized)
{
throw new BadRequestException("XSS injection detected from middleware.");
}
}
// rewind the stream for the next middleware
httpContext.Request.Body.Seek(0, SeekOrigin.Begin);
await _next.Invoke(httpContext);
}
}
不要忘记在Startup.cs下的Configure(...)中注册它:
app.UseMiddleware();
如果正在使用自定义异常处理中间件,这需要在那个之后注册。在中间件中,采取了与JSON转换器相同的方法来验证请求,并在验证失败时抛出一个错误请求异常。发现这比JSON转换器快大约50%,即使是一个单一字符串模型的构造示例也是如此。