ASP.Net Core中的AntiXSS

23

Microsoft Web Protection Library (AntiXSS)已经停止更新。该页面指出:“在.NET 4.0中,AntiXSS的一个版本被包括在框架中,并可以通过配置启用。在ASP.NET v5中,基于白名单的编码器将是唯一的编码器。”

我有一个典型的跨站脚本攻击场景:一个ASP.Net Core解决方案,用户可以使用所见即所得的HTML编辑器编辑文本。结果会显示给其他人看。这意味着如果用户在保存文本时注入JavaScript代码,当其他人访问页面时,这些代码可能会执行。

我想能够将某些HTML代码列入白名单(安全的代码),但过滤掉不良代码。

我应该如何做?我在ASP.Net Core RC2中找不到任何帮助我的方法。白名单编码器在哪里?我应该如何调用它?例如,我需要清理通过JSON WebAPI返回的输出。

7个回答

9

7

为了执行自动的Xss检查,旧版MVC使用了在System.Web.CrossSiteScriptingValidation类中实现的逻辑。然而,ASP.NET CORE 1中没有这个类。为了重用它,我复制了它的代码:

System.Web.CrossSiteScriptingValidation类

// <copyright file="CrossSiteScriptingValidation.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
public static class CrossSiteScriptingValidation
{
    private static readonly char[] StartingChars = { '<', '&' };

    #region Public methods

    // Only accepts http: and https: protocols, and protocolless urls.
    // Used by web parts to validate import and editor input on Url properties. 
    // Review: is there a way to escape colon that will still be recognized by IE?
    // %3a does not work with IE.
    public static bool IsDangerousUrl(string s)
    {
        if (string.IsNullOrEmpty(s))
        {
            return false;
        }

        // Trim the string inside this method, since a Url starting with whitespace
        // is not necessarily dangerous.  This saves the caller from having to pre-trim 
        // the argument as well.
        s = s.Trim();

        var len = s.Length;

        if ((len > 4) &&
            ((s[0] == 'h') || (s[0] == 'H')) &&
            ((s[1] == 't') || (s[1] == 'T')) &&
            ((s[2] == 't') || (s[2] == 'T')) &&
            ((s[3] == 'p') || (s[3] == 'P')))
        {
            if ((s[4] == ':') || ((len > 5) && ((s[4] == 's') || (s[4] == 'S')) && (s[5] == ':')))
            {
                return false;
            }
        }

        var colonPosition = s.IndexOf(':');
        return colonPosition != -1;
    }

    public static bool IsValidJavascriptId(string id)
    {
        return (string.IsNullOrEmpty(id) || System.CodeDom.Compiler.CodeGenerator.IsValidLanguageIndependentIdentifier(id));
    }

    public static bool IsDangerousString(string s, out int matchIndex)
    {
        //bool inComment = false;
        matchIndex = 0;

        for (var i = 0; ;)
        {

            // Look for the start of one of our patterns 
            var n = s.IndexOfAny(StartingChars, i);

            // If not found, the string is safe
            if (n < 0) return false;

            // If it's the last char, it's safe 
            if (n == s.Length - 1) return false;

            matchIndex = n;

            switch (s[n])
            {
                case '<':
                    // If the < is followed by a letter or '!', it's unsafe (looks like a tag or HTML comment)
                    if (IsAtoZ(s[n + 1]) || s[n + 1] == '!' || s[n + 1] == '/' || s[n + 1] == '?') return true;
                    break;
                case '&':
                    // If the & is followed by a #, it's unsafe (e.g. S) 
                    if (s[n + 1] == '#') return true;
                    break;

            }

            // Continue searching
            i = n + 1;
        }
    }

    #endregion

    #region Private methods

    private static bool IsAtoZ(char c)
    {
        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
    }

    #endregion
}

为了在所有请求中使用上述类,我创建了一个中间件,它使用CrossSiteScriptingValidation类:

AntiXssMiddleware

public class AntiXssMiddleware
{
    private readonly RequestDelegate _next;
    private readonly AntiXssMiddlewareOptions _options;

    public AntiXssMiddleware(RequestDelegate next, AntiXssMiddlewareOptions options)
    {
        if (next == null)
        {
            throw new ArgumentNullException(nameof(next));
        }

        _next = next;
        _options = options;
    }       

    public async Task Invoke(HttpContext context)
    {
        // Check XSS in URL
        if (!string.IsNullOrWhiteSpace(context.Request.Path.Value))
        {
            var url = context.Request.Path.Value;

            int matchIndex;
            if (CrossSiteScriptingValidation.IsDangerousString(url, out matchIndex))
            {
                if (_options.ThrowExceptionIfRequestContainsCrossSiteScripting)
                {
                    throw new CrossSiteScriptingException(_options.ErrorMessage);
                }

                context.Response.Clear();
                await context.Response.WriteAsync(_options.ErrorMessage);
                return;
            }
        }

        // Check XSS in query string
        if (!string.IsNullOrWhiteSpace(context.Request.QueryString.Value))
        {
            var queryString = WebUtility.UrlDecode(context.Request.QueryString.Value);

            int matchIndex;
            if (CrossSiteScriptingValidation.IsDangerousString(queryString, out matchIndex))
            {
                if (_options.ThrowExceptionIfRequestContainsCrossSiteScripting)
                {
                    throw new CrossSiteScriptingException(_options.ErrorMessage);
                }

                context.Response.Clear();
                await context.Response.WriteAsync(_options.ErrorMessage);
                return;
            }
        }

        // Check XSS in request content
        var originalBody = context.Request.Body;
        try
        {                
            var content = await ReadRequestBody(context);

            int matchIndex;
            if (CrossSiteScriptingValidation.IsDangerousString(content, out matchIndex))
            {
                if (_options.ThrowExceptionIfRequestContainsCrossSiteScripting)
                {
                    throw new CrossSiteScriptingException(_options.ErrorMessage);
                }

                context.Response.Clear();
                await context.Response.WriteAsync(_options.ErrorMessage);
                return;
            }

            await _next(context);
        }
        finally
        {
            context.Request.Body = originalBody;
        }            
    }

    private static async Task<string> ReadRequestBody(HttpContext context)
    {
        var buffer = new MemoryStream();
        await context.Request.Body.CopyToAsync(buffer);
        context.Request.Body = buffer;
        buffer.Position = 0;

        var encoding = Encoding.UTF8;
        var contentType = context.Request.GetTypedHeaders().ContentType;
        if (contentType?.Charset != null) encoding = Encoding.GetEncoding(contentType.Charset);

        var requestContent = await new StreamReader(buffer, encoding).ReadToEndAsync();
        context.Request.Body.Position = 0;

        return requestContent;
    }
}

你救了我的一天。 - Basil Kosovan
2
最好使用其他答案建议的推荐库,而不是复制粘贴不受支持的代码。 - Michael Freidgeim

5
如果您真正想要对输入进行清理,只允许一定的HTML元素,那么简单地编码内容并没有多大帮助。您需要一个HTML清洁器。
构建这样的工具并不容易。您需要某种方法来解析HTML,并有一组规则来确定允许通过什么以及不允许通过什么。为了防止未来新的HTML标签引起安全问题,我建议采用白名单方法。
至少有两个开源HTML清理库可以在.NET Core上使用,其中一个是我几年前写的。两者都可作为NuGet软件包提供: 它们使用不同的HTML解析器作为后端。您可能需要微调规则集,以匹配您的所见即所得编辑器创建的内容。

谢谢!这对我在自问自答的问题中找到正确方向非常有帮助: https://stackoverflow.com/questions/59483284/passing-raw-html-from-view-to-controller-xss-safe-no-information-loss - om-ha
HtmlSanitizer比HtmlRuleSanitizer更受欢迎,也更活跃地维护。 - Michael Freidgeim

4
你可以在.NET Standard中使用System.Text.Encodings.Web进行编程编码。它提供HTML、JavaScript和URL编码器。它应该等同于AntiXss,因为它被记录}为使用白名单:

默认情况下,编码器使用安全列表,仅限于基本拉丁Unicode范围,并将该范围之外的所有字符编码为它们的字符代码等效项。


2
听起来您需要一种基于白名单的过滤器。OWASP AntiSamy.NET曾经可以做到这一点,但我认为它已经不再维护了。 如果数据始终以JSON形式传递,您还可以在客户端使用DOMPurify进行处理,然后将其添加到DOM中。在JSON本身中存在恶意HTML并不是很危险(至少只要您正确设置content-type和X-content-type-options:nosniff标头)。代码不会触发,直到被渲染到DOM中。

除了一些电子邮件(在那里我可以作弊并删除所有<[^>]+>),我可以通过像DOMPurify这样的东西运行所有内容。然而,不要将所有内容都通过JSON运行,有时它也直接写入页面。用<script>document.write(DOMPurify(UnBase64(@Html.Raw(Base64(Model.Foobar)))));</script>替换每个@Html.Raw(Model.Foobar)将是很多工作。(必须对数据进行base64编码,因为其中任何<script></script>标记都将由HTML解析器执行,并且HtmlEncode将禁用您想要保留的标记。+一些utf8问题。) - Tedd Hansen

1
这是一个很好的问题。我想指出的一件事是,我们不应该尝试构建自己的消毒剂。它们很难正确使用。最好使用由知名作者构建和维护的库。 来自OWASP: "OWASP建议使用安全重点编码库,以确保这些规则得到正确实施。"
如果您正在使用.NET Framework,则此库仍可能适用: https://learn.microsoft.com/en-us/dotnet/api/system.web.security.antixss.antixssencoder?view=netframework-4.8 对于.NET Core,如上所述,System.Text.Encodings库也可能有所帮助。 https://learn.microsoft.com/en-us/aspnet/core/security/cross-site-scripting?view=aspnetcore-2.2#accessing-encoders-in-code

1

NuGet包最后更新于2020年6月13日。 - Michael Freidgeim
我认为这个软件包运行稳定,不需要经常更新。但是我也在该存储库上发布了一个问题:https://github.com/hangy/AntiXss/issues/150 - Will Huang
版本v0.1.16(小于1)通常意味着作者不认为该软件包是稳定的。 - Michael Freidgeim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接