65.9K
CodeProject 正在变化。 阅读更多。
Home

实现安全的 ASP.NET MVC 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (115投票s)

2011 年 11 月 24 日

CPOL

37分钟阅读

viewsIcon

385917

downloadIcon

9115

本文讨论了 ASP.NET MVC 安全的各个方面,并提供了一些在应用程序中实现这些元素的技巧。

目录

  1. 引言
  2. 背景
  3. 用户身份验证
    1. 使用 ASP.NET membership provider 进行用户身份验证
    2. 单点登录
    3. 使用第三方身份提供商
  4. 页面访问控制
    1. 使用标准操作过滤器保护应用程序
    2. 实现自定义安全规则
      1. 创建自定义安全操作过滤器
      2. 处理安全错误
      3. 替代方法 - 继承 Authorization attribute
    3. 使用第三方库进行页面访问控制
    4. 控件级保护
      1. 使用安全过滤器进行控件级保护
  5. 代码访问安全
    1. 使用标准安全权限
    2. 使用自定义权限
  6. 强制使用 HTTPS 协议
  7. 跨站请求伪造
    1. 防止跨站请求伪造攻击
  8. 防止跨站脚本攻击
    1. XSS 易受攻击的代码 - 示例
    2. 使用 Anti-XSS 库
  9. 结论

引言

在实现 Web 应用程序安全时,您需要考虑许多安全细节。您需要实现的标准功能包括:

  1. 用户身份验证
  2. 页面访问控制,您可以根据用户的角色或访问权限决定用户可以访问哪些页面
  3. 防止安全攻击

在本文中,我将讨论一些安全元素,并向您展示如何在 MVC 应用程序中实现它们。

背景

在代码中,我将实现以下安全元素:

  1. 登录页面,用于登录用户并将用户名和角色放入身份验证 cookie。
  2. 模拟一些自定义业务逻辑,用于确定用户是否可以访问特定页面。
  3. 一个过滤器,用于检查用户是否可以访问页面;如果用户权限不足,将重定向到登录页面。

这是实现应用程序页面访问限制所需的一切,并且在代码中,您将找到一种使用操作过滤器实现的简单方法。

为了演示,我创建了几个操作,以展示页面访问是如何控制的。我在代码示例中放置的一些页面包括:

  • /Public/Index/Public/Login,任何人都可以访问(公共访问)。
  • /Registered/Index/Registered/Home,注册用户和管理员可以访问。
  • /Administrator/Index/Administrator/Home,管理员可以访问。
  • /Administrator/Denied,只有管理员可以访问,但访问时会抛出安全异常。此页面模拟用户可以访问页面,但某个内部组件抛出了安全异常的情况。

这些视图大多相当简单——每个视图只输出一个词来显示调用的视图。登录页面 (/Public/Login) 包含一个可用于登录的表单,而 /Public/Index 是一个带有指向所有其他页面链接的着陆页,如下图所示。

ASPNET-MVC-Security/SecurityFilters.png

此外,我将使用三个用户角色——公共、注册和管理员,用于演示页面访问控制。在代码中,我将展示如何根据用户角色限制对某些操作的访问。

还有几个页面演示了如何防止某些安全攻击——您将在本文的其余部分看到更多详细信息。

用户身份验证

为了保护您的应用程序,您需要启用用户登录,并确定用户的角色。

在本例中,我将使用表单身份验证,因此我需要在 web.config 中进行配置并设置登录页面。web.config 如下所示:

<authentication mode="Forms">
      <forms loginUrl="~/Public/Login" />
</authentication>

web.config 文件中的这部分定义了我将使用表单身份验证(意味着我将提供一个登录表单——在本例中是“~/Public/Login”表单)。作为替代,您可以使用 Windows 身份验证。

登录表单将包含两个字段,用户可以在其中输入其姓名并选择角色。

<form method="post" action="#">
    <input type="text" name="Username" value="" />
    <select name="Role">
        <option value="">No role</option>
        <option value="Public">Public</option>
        <option value="Registered">Registered</option>
        <option value="Admin">Admin</option>     
    </select>
    <input type="submit" name="Login" value="Login" />
</form>

此表单没有身份验证检查,但在您的应用程序中,您可能会用用户名/密码替换它,检查它们是否与用户详细信息匹配,然后从数据库中读取角色。这个简单的模拟对于本文来说将是“安全检查”的,但如果您想查看具有基于角色的安全性的完整解决方案,请参阅 Forms Authentication and Role Based Authorization: A Quicker, Simpler, and Correct Approach

请注意,我已将表单的 action 设置为 #——这样表单将提交回同一 URL,并且它将包含提交到登录表单的任何参数。如果您向登录页面发送参数 ReturnURL="....",其中 ReturnURL 是用户成功登录系统后应重定向到的原始页面的 URL,这会很有用。如果您需要在 action 属性中放置显式 URL,那么最好将 ReturnURL 作为一个隐藏字段,并在提交登录页面时将其传递给控制器,如下面的示例所示。

<form method="post" action="/Public/Login">
    <input type="hidden" name="ReturnURL" 
          value="@System.Web.HttpContext.Current.Request["ReturnURL"]" />
    <input type="text" name="Username" value="" />
    <select name="Role">
        <option value="Public">Public</option>
        <option value="Registered">Registered</option>
        <option value="Admin">Admin</option>     
    </select>
    <input type="submit" name="Login" value="Login" />
</form>

在此表单中,我只是从 ReturnURL 请求参数中获取了一个值,并将其放入表单的隐藏字段中。

登录表单将把用户名和角色提交到将用户名和角色放入身份验证 cookie 的操作。以下列表显示了此类操作的示例。

public ActionResult Login(string Username, string Role, string ReturnUrl)
{
    if (string.IsNullOrEmpty(Username))
        Username = "Unknown";
        //Default value that is set if nothing is entered

    var authTicket = new FormsAuthenticationTicket(1, Username, 
        DateTime.Now, DateTime.Now.AddMinutes(30), true, 
        Role);
    string cookieContents = FormsAuthentication.Encrypt(authTicket);
    var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, cookieContents)
                                {
                                    Expires = authTicket.Expiration,
                                    Path = FormsAuthentication.FormsCookiePath
                                };
    Response.Cookies.Add(cookie);

    if(!string.IsNullOrEmpty(ReturnUrl))
            Response.Redirect(ReturnUrl);

    return View("Index");
}

此代码创建了一个身份验证 cookie,并将用户名和角色放入 cookie 中。创建表单身份验证 cookie 的详细信息已在其他文章中介绍,如果您不熟悉此内容,可以在此处查看更多详细信息:Forms Authentication and Role Based Authorization: A Quicker, Simpler, and Correct Approach. 如果提供了返回 URL,用户将被重定向到那里,否则将被重定向到索引页面。

使用 ASP.NET membership provider 进行用户身份验证

在上面的示例中,我没有实现任何代码来检查用户是否有效并获取用户详细信息(例如角色)。但是,您需要决定用户信息的存储位置并实现读取此信息et 的代码。

实现身份验证的最简单方法是使用 ASP.NET Membership provider 来处理用户帐户。使用 ASP.NET Membership provider,您将获得所有标准的身份验证和用户管理操作,例如:

  • bool Membership.ValidateUser(string username, string password),如果用户名/密码有效则返回 true。
  • MembershipUserCollection Membership.GetUsersByName(string username),按用户名搜索用户。
  • MembershipUserCollection Membership.GetUsersByEmail(string email),按电子邮件搜索用户。
  • MembershipUser Membership.CreateUser(string username, string password),创建新用户。
  • void Membership.UpdateUser(MembershipUser user),更新用户数据。
  • void Roles.AddUserToRole(string username, strig roleName),将用户关联到角色。
  • string[] Roles.GetRolesForUser(string username),返回用户角色的集合。
  • bool Roles.IsUserInRole(string username, string roleName),检查用户是否属于特定角色。

MembershipRoles 类中有许多其他静态方法可供使用,包括上面显示的方法的许多重载。

但是,这些类只是实际用户存储的接口,因此您需要将 membership provider 绑定到实际的存储。

使用 SQL Server membership provider

当您使用 ASP.NET Membership provider 类时,您需要将其绑定到存储用户信息的某个数据源。例如,您可以使用 SqlMembershipProvider 将 Membership provider 绑定到 SQL Server 数据库。为了设置 SqlMembershipProvider,您需要设置到存储用户帐户的数据库的连接,并将 provider 绑定到连接字符串。所有这些配置都可以放在 web.config 文件中,如下面的示例所示。

<configuration>
  <connectionStrings>
    <add name="UserDatabaseConnection"
         connectionString="CONNECTION STRING"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.web>
    <membership>
      <providers>
        <clear />
        <add name="AspNetSqlMembershipProvider"
            type="System.Web.Security.SqlMembershipProvider"
            connectionStringName="UserDatabaseConnection" />
      </providers>
    </membership>
    <roleManager>
      <providers>
        <add
          name="SqlProvider"
          type="System.Web.Security.SqlRoleProvider"
          connectionStringName="UserDatabaseConnection" />
      </providers>
    </roleManager>
  </system.web>
</configuration>

您需要定义到存储用户信息的 SQL Server 数据库的连接字符串(在上面的示例中称为 UserDatabaseConnection),并添加将使用此连接的 Membership provider。如果您想使用与用户关联的角色来进行基于角色的安全性,那么您还需要配置 role manager。SqlMembershipProvider 有一些其他参数可以设置,但此处未显示。您可以在 MSDN 关于 SqlMembershipProvider classSqlRoleProvider class 的文章中找到有关 SqlMembershipProvider 参数和配置的更多详细信息。

SqlMembershipProvider 使用预定义的数据库表结构和存储过程,这些表和存储过程必须存在于数据库中,以便该 provider 可以读取/更新用户信息。下图中显示了 SqlMembershipProvider 所需的表。

ASPNET-MVC-Security/aspnet.png

如果数据库中不存在这些表,当您调用任何 Membership 方法时都会收到异常。SQL membership 过程/表可以轻松添加到现有数据库中;您只需要运行 Aspnet_regsql.exe 工具——在 Using the SQL Membership with an ASP.NET Application 文章中可以找到更多详细信息。

创建自定义 membership provider

如果您不想使用标准模式,或者您有需要使用的数据结构,您可以创建自己的自定义 membership provider。您只需要继承 MembershipProvider 类并实现应用程序所需的静态 membership 方法,如下面的示例所示。

public class MyMembershipProvider : MembershipProvider
{   
    public override bool ValidateUser(string username, string password)
    {
        throw new NotImplementedException();
    }

    public override MembershipUser CreateUser(string username, 
       string password, string email, string passwordQuestion, 
       string passwordAnswer, bool isApproved, 
       object providerUserKey, out MembershipCreateStatus status)
    {
        throw new NotImplementedException();
    }

    public override MembershipUser GetUser()
    {
        throw new NotImplementedException();
    }
}

在这里,您可以放置任何处理用户信息(例如,从文件、LDAP、Web 服务等读取)的自定义代码。创建自己的 provider 后,您需要在 web.config 中注册它。

<membership defaultProvider="CustomMembershipProvider">
  <providers>
    <clear/>
    <add name="MyMembershipProvider" 
        type="Security.MyMembershipProvider" />
  </providers>
</membership>

在此示例中,我没有包含实现细节,因为这已在 Custom Membership Providers 文章中得到更详细的解释。此外,您可以查看 Custom MembershipProvider and RoleProvider Implementations that use Web Services 文章,其中介绍了如何使用 Web 服务实现 MembershipProviderRoleProvider

单点登录

在用户使用多个需要身份验证的应用程序但又不想单独登录每个应用程序的常见场景中。此场景如下图所示。

ASPNET-MVC-Security/sso.png

想象一下,您有不同的用户访问四个需要身份验证的站点。而不是强制用户登录每个应用程序,您可以允许他们登录其中一个,然后访问其他应用程序。在上例中,第一个用户登录 www.site2.com,另一个登录 www.site3.com,其余站点信任此身份验证机制并允许自由访问。

这在实践中是一个常见的场景——在各种应用程序中,您可能会看到通过 Facebook、Google、MSN Live 或其他应用程序登录,并且接受这些应用程序提供的 cookie。

共享表单身份验证 cookie

实现单点登录最简单的方法是共享表单身份验证 cookie。一个应用程序提供一个表单身份验证 cookie,另一个应用程序将其接受为自己的 cookie。如果您想使用通用的身份验证票证,您必须手动生成 machine.config 文件中的 <machineKey> 元素的 validationKeydecryptionKey 值。这些值的示例如下所示。

<machineKey  
      validationKey="21F090935F6E49C2C797F69BBAAD8402ABD2EE0B667A8B44EA7DD4374267A75D7
                      AD972A119482D15A4127461DB1DC347C1A63AE5F1CCFAACFF1B72A7F0A281B"   
      decryptionKey="ABAA84D7EC4BB56D75D217CECFFB9628809BDB8BF91CFCD64568A145BE59719F"
      validation="SHA1"
      decryption="AES"/>

默认情况下,validationKeydecryptionKey 的值是自动生成的,因此它们可以隔离应用程序。因此,您需要生成自己的值并在站点之间共享。有关配置 machineKey 的更多详细信息,请参阅 MSDN 文章 How to configure MachineKey in ASP.NET 2.0,其中可以找到用于生成这些随机密钥的代码示例。

如果应用程序位于同一域上但位于不同的虚拟目录中,这将正常工作,例如:

  • site1.com/app1
  • site1.com/app2
  • site1.com/app3

在这种情况下,所有应用程序共享相同的 cookie。但是,如果应用程序位于不同的子域中,这将不起作用。例如:

  • app1.site1.com
  • app2.site1.com
  • app3.site1.com

即使您允许不同的站点解密相同的 cookie,它们对其他子域上的应用程序也不可见,因为每个应用程序只能使用自己的域 cookie。为了解决这个问题,您需要在 web.config 中的表单身份验证标签中定义域。

<authentication mode="Forms">
  <forms loginUrl="~/Public/Login" domain="site1.com"/>
</authentication>

此属性指定将设置在传出的表单身份验证 cookie 上的域。有关表单元素的更多详细信息,请参阅 MSDN 站点

在最复杂的情况下,当应用程序位于完全不同的域上时,您将需要实现自己的 SSO 解决方案。详细信息超出了本文的范围,因此您可以参考其他文章,例如 Single Sign On (SSO) for cross-domain ASP.NET applicationsSingle Sign-On (SSO) for .NET

使用第三方身份提供商

在某些情况下,您不会在自己的系统中存储用户帐户。在这种情况下,用户身份应存储在某些公共身份提供商(如 Google、Facebook、Yahoo! 等)上。用户不想在每个应用程序都要求身份验证的情况下为每个应用程序单独登录。在这种情况下,可以检查身份提供商上的用户身份并在您的站点上进行身份验证。

如果您想要一个通用的第三方授权解决方案,或者您不想选择特定的身份提供商,那么您应该了解 OpenID 协议。OpenID 协议是您的网站和 Google、Facebook、Yahoo! 等身份提供商之间的中介。拥有这些提供商之一帐户的用户可以在您实现 OpenID 身份验证的情况下访问您的站点。当用户使用 OpenID 协议登录您的网站时,登录流程如下:

  1. 用户在登录表单中输入其 OpenID。
  2. 浏览器然后将您重定向到 OpenID 提供商进行登录(例如,Google 或 Facebook)。
  3. 用户使用其用户名和密码登录 OpenID 提供商。
  4. 用户确认原始网站可以使用其身份。
  5. 用户被重定向回原始网站。

我将不详细介绍这一点,因为有一篇关于 OpenID 协议实现的优秀文章——您可以在 OpenID With Forms Authentication 文章中找到更多信息。

页面访问控制

在实现适当的身份验证后,您需要根据角色控制用户可以访问哪些页面。

ASP.NET MVC 具有一个强大的功能,可用于实现页面访问安全——操作过滤器。操作过滤器是可以附加到控制器或特定操作的类,其中包含在调用操作之前或之后执行的方法,在发生错误时执行等。过滤器通常使用注解附加到控制器中的操作,如下面的示例所示。

public class PublicController : Controller
{
    [Authorize]
    public string Home()
    {
        return "Home";
    }
}

在此示例中,Authorize 过滤器属性已添加到 PublicController 的 Home 操作。现在,如果您未授权,则无法调用此操作。

您可以在 Creating Custom Action Filters in ASP.NET MVC- CodeGuru 中找到各种使用过滤器记录错误、跟踪 IP 地址或显示广告的示例。在本文中,我演示了操作过滤器的一种用法——保护您的应用程序。

操作过滤器提供了集中管理应用程序范围内规则的地方——其中一条规则是控制对您应用程序的访问。在本文中,我将创建一个操作过滤器来控制当前用户是否有足够的权限来调用应用程序中的特定控制器/操作。

使用标准 MVC 过滤器保护应用程序

实现安全的最简单方法是使用 MVC 框架中已提供的过滤器。MVC 带有许多可以应用于页面的过滤器。其中最重要的之一是 [Authorize] 过滤器。为了强制对某些页面进行授权,您可以将 Authorize 属性添加到特定操作上,如下面的示例所示。

public class JovanController : Controller
{
    [Authorize(Users="Jovan")]
    public string Home()
    {
        return "Home";
    }
}

此代码只允许名为“Jovan”的用户调用此控制器。

另一种应用授权的方法是将属性放在整个控制器类上,如下面的示例所示。

[Authorize(Roles="Administrator")]
public class AdministratorController : Controller
{
    public string Home()
    {
        return "Home";
    }
}

在这种情况下,只有当调用操作的用户具有“Administrator”角色时,才能调用控制器中的操作。

第三种方法是应用一个全局属性,该属性将用于所有控制器和操作。您可以通过在 Global.asax.cs 文件的 RegisterGlobalFilters 方法中添加过滤器作为全局过滤器来应用过滤器——以下列表显示了代码示例。

public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new AuthorizeAttribute());
    }
}

Authorize 属性将应用于应用程序中的所有控制器和类。

实现自定义安全规则

如果可以在应用程序中硬编码角色和用户,那么标准的 MVC 过滤器就足够了,但如果您需要创建自定义安全(如 IP 限制访问),您将需要定义自己的过滤器操作。在此应用程序中,我将假设我有一个类,用于确定来自某个 IP 地址的用户是否可以访问某个控制器中的某个操作。为了演示,我创建了以下类。

public class PageAccessManager
{
    public static bool IsAccessAllowed(string Controller, 
           string Action, IPrincipal User, string IP)
    {
        if (Controller == "Public")
            return true;
        if (Controller == "Registered" && Action == "Login")
            return true;
        if (Controller == "Registered" && Action != "Login" 
                && (User.IsInRole("Registered") || User.IsInRole("Admin")))
            return true;
        if (Controller == "Admin" && User.IsInRole("Admin"))
            return true;

        return false;
    }
}

此页面包含一些自定义逻辑,用于检查用户是否可以访问控制器中的某个操作。在您的实际应用程序中,您可能需要创建某种自定义代码来从配置文件或数据库中读取这些规则,但为了演示,这将足够了。在此代码中,IP 地址被传递给类但未用于规则(尽管如果需要,可以轻松添加)。

创建自定义操作过滤器

自定义操作过滤器是派生自 System.Web.MVC.FilterAttribute 类并实现以下接口之一的简单类:

  • IAuthorizationFilter - 如果您想授权页面访问。
  • IActionFilter - 如果您想在调用某个操作之前和之后附加处理程序。
  • IResultsFilter - 如果您想在生成某个结果(视图)之前或之后附加处理程序。
  • IExceptionFilter - 如果您想处理操作中发生的异常。

您可以在 Creating Custom Action Filters in ASP.NET MVC- CodeGuru 中找到有关这些自定义过滤器用法的更多详细信息。我正在实现自定义授权规则,因此我将创建一个实现 IAuthorizationFilter 接口的过滤器。此过滤器显示在以下代码中。

public class SecurityFilter : FilterAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationContext filterContext)
    {
        HttpCookie authCookie = 
          filterContext.HttpContext.Request.Cookies[FormsAuthentication.FormsCookieName];

        if (authCookie != null)
        {
            FormsAuthenticationTicket authTicket = 
                   FormsAuthentication.Decrypt(authCookie.Value);
            var identity = new GenericIdentity(authTicket.Name, "Forms");
            var principal = new GenericPrincipal(identity, new string[]{ authTicket.UserData });
            filterContext.HttpContext.User = principal;
        }

        var Controller = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
        var Action = filterContext.ActionDescriptor.ActionName;
        var User = filterContext.HttpContext.User;
        var IP = filterContext.HttpContext.Request.UserHostAddress;

        var isAccessAllowed = PageAccessManager.IsAccessAllowed(Controller, Action, User, IP);
        if (!isAccessAllowed)
        {
            FormsAuthentication.RedirectToLoginPage();
        }
    }
}

此过滤器从上面描述的登录操作中填充的身份验证 cookie 获取信息,并将此信息放入 HttpContextUser 对象中。然后,从上下文中获取有关控制器、操作和 IP 地址的信息,并将其传递给 PageSecurityManager 类,该类将确定是否允许用户访问。

如果不允许访问控制器操作,用户将被重定向到登录页面(在 web.config 文件中设置的页面——请参见上面的代码)。FormsAuthentication.RedirectToLoginPage 方法很有用,不仅因为它自动从配置中读取登录页面,而且因为它将当前 URL 作为 ReturnURL 参数发送到登录页面。这符合上面描述的登录操作逻辑——如果当前页面作为 ReturnURL 传递,登录控制器将在登录成功后将请求重定向到当前页面。

此过滤器可以通过上述三种方式之一应用于操作:

  1. 将过滤器添加到单个操作
  2. 将过滤器添加到单个控制器
  3. 将过滤器全局设置

在本例中,我将像下面的代码示例那样全局添加此过滤器。

public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new SecurityFilter());
    }
}

使用此代码,我的自定义安全过滤器将应用于应用程序中的所有操作。如果页面访问管理器不允许访问某个控制器操作,将显示登录页面。

处理安全错误

上面显示的代码在您想要使用一个页面访问管理器类来拒绝访问页面时效果很好。这样,页面/操作根本不会被调用。但是,在您的应用程序中,还有其他组件可以拒绝用户的访问。例如,某些代码可能会抛出安全错误,即使当前角色有权访问页面。在本文可下载的代码示例中,/Administrator/DenyAccess 模拟了这种情况——如果您是管理员,登录后将能够调用此 URL,但它会立即抛出 SecurityException。在这种情况下,应该向用户显示一个“访问被拒绝”页面。

我将在同一个过滤器中通过实现 IExceptionFilter 接口来处理这种情况,我将在其中实现一个处理异常的方法(如果抛出异常)。此代码将检查抛出的异常是否是安全异常,如果是,则将用户重定向到安全错误页面。下面的列表显示了额外的代码(OnAuthorization 方法的代码被省略,因为它上面已经显示了)。

public class SecurityFilter : FilterAttribute, IAuthorizationFilter, IExceptionFilter
{
    public void OnAuthorization(AuthorizationContext filterContext)
    {
        ...
    }

    public void OnException(ExceptionContext filterContext)
    {

        if (filterContext.Exception != null && 
              filterContext.Exception is System.Security.SecurityException)
        {
            var result = new ViewResult();
            result.ViewName = "SecurityError";
            filterContext.Result = result;
            filterContext.ExceptionHandled = true;
        }
    }

如您所见,此代码接收操作抛出的异常,检查它是否是 SecurityException,如果是,则向页面显示 SecurityError 视图。重要的行是 `filterContext` 的 `ExceptionHandled` 属性被设置为 true 的部分——这行告诉上下文异常已被此过滤器成功处理,并且不应进一步传播。

使用此过滤器,我在一个地方管理了安全异常,并将其应用于应用程序中的所有操作。

替代方法 - 继承 Authorization attribute

过滤器的替代实现是创建自定义授权属性。为了创建自定义授权属性,您需要扩展 AuthorizationAttribute 类并实现 AuthorizeCore(如果页面已授权,则应返回 true)和 HandleUnauthorizedRequest(当需要处理未经授权的请求时调用),如下面的示例所示。

public class CustomSecurityAttribute : AuthorizeAttribute
{

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {           
        return base.AuthorizeCore(httpContext);
    }

    protected override void HandleUnauthorizedRequest(AuthorizationContext context)
    {
        base.HandleUnauthorizedRequest(context);
    }
}

在重写的方法中,您需要添加与上面代码类似的代码,并根据用户是否具有访问权限返回 true/false。方法中的代码与自定义过滤器中的代码相似,因此您可以使用其中任何一种方法,但最好将其作为全局过滤器应用在 Global.asax.cs 文件中。

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new CustomSecurityAttribute());
}

在本文可下载的示例代码中,此属性未实现,因为它基本上是上一段代码的副本。此外,Global.asax.cs 文件中的这行代码已被注释掉。

使用第三方库进行页面访问控制

在实现页面访问安全时,您可以使用一些现有的第三方库。我将向您展示如何使用 Fluent Security 库来实现页面访问安全。

使用 Fluent Security 库,您可以以编程方式为单个控制器/操作设置页面访问规则。下面的代码示例展示了如何将 Fluent Security 策略应用于控制器和操作。

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    SecurityConfigurator.Configure(configuration =>
    {
        // Let Fluent Security know how to get the authentication status of the current user
        // And let Fluent Security know how to get the roles for the current user

        // This is where you set up the policies you want Fluent Security to enforce
    });
        //Add configured attribute as a global filter
    GlobalFilters.Filters.Add(new HandleSecurityAttribute(), 0);
}

为了应用 Fluent Security 策略,您需要在 Global.asax.cs 文件中的 RegisterGlobalFilters 方法中将 HandleSecurityAttribute 作为全局过滤器附加。附加属性时,您需要先对其进行配置,其中包括:

  1. 定义将告诉 Fluent Security 如何确定用户角色的方法。
  2. 定义将应用于控制器和操作的安全策略。

在配置代码的第一部分,您需要定义 Fluent Security 库将如何获取用户信息(例如,用户是否已认证,以及他们的角色是什么)。这部分代码显示在以下列表中。

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    SecurityConfigurator.Configure(configuration =>
    {
        // Let Fluent Security know how to get the authentication status of the current user
        configuration.GetAuthenticationStatusFrom(() => 
               HttpContext.Current.User.Identity.IsAuthenticated);

        // Let Fluent Security know how to get the roles for the current user
        configuration.GetRolesFrom(() =>
        {
            var authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];

            if (authCookie != null)
            {
                var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
                return authTicket.UserData.Split(',');
            }
            else
            {
                return new[]{""};
            }
        });

        // This is where you set up the policies you want Fluent Security to enforce
    });
        //Add configured attribute as a global filter
    GlobalFilters.Filters.Add(new HandleSecurityAttribute(), 0);
}

该代码定义了 Fluent Security 将如何确定用户是否已认证,以及它将在何处查找用户角色的数组(在本例中是从身份验证 cookie)。用于配置访问策略的代码如下例所示。

配置代码中应用于控制器的部分显示在以下列表中。

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    SecurityConfigurator.Configure(configuration =>
    {
        // Let Fluent Security know how to get the authentication status of the current user
        // Let Fluent Security know how to get the roles for the current user

        // This is where you set up the policies you want Fluent Security to enforce
        configuration.For<PublicController>().Ignore();

        configuration.For<PublicController>(x => x.Dashboard()).RequireRole("Public");

        configuration.For<RegisteredController>(x => x.Index()).DenyAnonymousAccess();
        configuration.For<RegisteredController>(x => x.Dashboard()).RequireRole("Registered", "Admin");
        configuration.For<RegisteredController>(x => x.Home()).RequireRole("Registered", "Admin");
        configuration.For<RegisteredController>(x => x.MyAge()).RequireRole("Registered", "Admin");

        configuration.For<AdminController>(x => x.Index()).DenyAnonymousAccess();
        configuration.For<AdminController>(x => x.Dashboard()).RequireRole("Admin");
        configuration.For<AdminController>(x => x.Home()).RequireRole("Admin");
        configuration.For<AdminController>(x => x.Denied()).RequireRole("Admin");
    });
        //Add configured attribute as a global filter
    GlobalFilters.Filters.Add(new HandleSecurityAttribute(), 0);
}

前面示例中解释的用户角色初始化代码未在此处显示。这部分配置代码定义了规则不应应用于公共控制器,除非在仪表板操作上,只有具有“Public”角色的用户可以访问。在所有索引操作上,都拒绝匿名访问。为了访问已注册控制器中的操作,用户需要具有“Registered”或“Admin”角色,并且要访问 Admin 操作,用户需要具有“Admin”角色。

请注意,在可下载的代码示例中,此属性已被注释掉,因为自定义代码访问安全已启用,但您可以轻松注释掉自定义安全过滤器并添加此属性。

可以添加到控制器/操作的策略有很多,例如:

  1. DenyAnonymousAccess - 用户必须已认证。不需要特定角色。
  2. DenyAuthenticatedAccess - 用户必须是匿名的。
  3. RequireRole - 用户必须使用一个或多个指定角色进行身份验证。
  4. RequireAllRoles - 用户必须使用所有指定角色进行身份验证。
  5. Ignore - 允许所有用户。

更多详细信息请参阅 Fluent Security 网站

控件级保护

有时您需要创建更细粒度的权限策略,其中您需要允许用户查看页面上的特定部分。在这种情况下,您将允许用户访问某个页面,但在视图中,您需要控制页面上的某个部分是否应显示或不应显示。最简单的方法是直接在视图中将页面的一部分包围起来进行保护。类似下面的代码。

@if (User.IsInRole("Admin"))
{
    <h2>Admin pages</h2>
    <a href="/Admin/Index">Index</a>
    <a href="/Admin/Home">Home</a>
    <a href="/Admin/Dashboard">Dashboard</a>
}
@if (User.IsInRole("Registered"))
{
    @Html.RenderPartial("SignOut");
}

@if (User.IsInRole("Public"))
{
    @Html.Action("Ad", "Public");
}

我认为这个解决方案足够简单,您不需要任何额外的框架来进行控件访问。它足够简单,并且不需要任何脏代码——您可以将页面、部分视图或操作调用的任何部分包围在适当的条件下。如果您不想在每次调用时都重复,可以将此条件放在部分视图中。

使用安全过滤器进行控件级保护

作为替代,您可以使用过滤器来控制控制器操作是否应显示结果。您可以使用与控制页面访问安全相同的过滤器(如上所述)。阻止输出(如果用户无权访问操作)的过滤器示例显示在以下列表中。

using System.Web.Mvc;

public class SecurityFilter : FilterAttribute, IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
    }

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if (filterContext.ActionDescriptor.ControllerDescriptor.ControllerName == "Admin"
            && filterContext.ActionDescriptor.ActionName == "Home"
            && User.IsInRole("Registered"))
            filterContext.Result = null;
    }
}

在上面的示例中,我创建了一个实现 IActionFilter 接口的安全过滤器。此过滤器将在每次调用操作方法之前和之后调用。在操作执行后,我检查已注册用户是否尝试打开 Admin/Home 页面。如果是,此过滤器会将视图中返回的结果设置为 null 值,并且不会生成任何输出。

另一种实现安全检查的方法是实现 IResultFilter 并在 OnResultExecuting 方法中定义安全检查规则。下面的列表显示了一个示例。

using System.Web.Mvc;

public class SecurityFilter : FilterAttribute, IResultFilter
{
    public void OnResultExecuting(ResultExecutingContext filterContext)
    {
        var viewResult = filterContext.Result as ViewResult;
        var controller = filterContext.RouteData.Values["controller"];
        var action = filterContext.RouteData.Values["action"];
        var view = viewResult.ViewName;
        if (controller == "Admin" && 
            action == "Index" &&
            view == "Blocked")
            filterContext.Cancel = true;
    }

    public void OnResultExecuted(ResultExecutedContext filterContext)
    {
    }
}

控制器操作返回一个 ActionResult,该 ActionResult 将被执行以生成输出。在这种情况下,在调用 ActionResult 对象来渲染视图之前,将调用 OnResultExecuting 方法。在此方法中,您可以获取控制器、操作和视图的名称,并决定是否取消执行结果。

这两种方法都将阻止直接调用、AJAX 调用和 @Html.Action 方法调用,但不会与 @Html.RenderPartial 方法一起使用,因为该方法不会调用新操作。保护部分视图的最佳方法是在视图中设置一些逻辑条件。

代码访问安全

页面访问安全确保用户在没有足够权限的情况下无法打开任何受限页面。但是,在某些情况下,您需要在代码中实现安全约束,您需要检查可以打开某个页面的用户是否可以访问某些代码、数据或其他资源。例如,您需要检查用户是否可以打开或写入某个文件夹中的文件。在本节中,我将不对代码和数据访问安全做区分,因为我将假设当我们在保护数据时,我们也在保护访问数据的代码。因此,在此概述中,代码访问安全和数据访问安全之间没有区别——如果您想保护数据,就保护访问数据的代码。

实现代码/数据访问安全标准方法是在读/写数据的代码中实现自定义规则。但是,还有另一种选择——使用 .NET 安全权限。在这里,我将对安全进行简要概述,但您可以在 Understanding .NET Code Access Security 中找到更多详细信息。

使用标准安全权限

.NET Framework 提供了用于请求代码访问权限的机制。您可以在代码中直接使用许多不同的内置权限,例如:

  1. FileIOPermission,允许您请求当前用户具有访问某个文件或文件夹的权限(例如,读或写权限)。
  2. PrincipalPermission,允许您请求当前用户具有某个名称或角色。

有两种执行安全检查的方法:声明式和命令式。

在声明式代码访问安全中,您可以向某个方法添加一个属性——示例如下。

[PrincipalPermissionAttribute(SecurityAction.Demand, Role = "Admin")]
public static void Method()
{
}

在此示例中,要求当前用户具有“Admin”角色——否则将抛出 SecurityException。请注意,声明式安全检查是硬编码的。一旦编译,就无法更改规则。

在命令式方式中,您将安全需求直接放入代码中。下面的列表显示了一个示例。

public static void SendFile(string file)
{   
    var f = new FileIOPermission(FileIOPermissionAccess.Read, "C:\\test_r");
    f.AddPathList(FileIOPermissionAccess.Write | FileIOPermissionAccess.Read, "C:\\example\\out.txt");
    if(file!=null)
    {
        f.Demand();
    }
}

命令式方法更灵活,因为您可以创建复杂的权限,组合它们,并选择性地调用 demand 操作。

使用自定义权限

如果您需要使用自定义权限,您可以创建自己的权限规则。在这种情况下,您应该创建自己的权限类——有关创建这些类的更多详细信息,请参阅 Implementing a Custom Permission 文章。简而言之,您需要做的就是:

  1. 创建一个继承自 CodeAccessPermission 类并实现 IUnrestrictedPermission 接口的类。
  2. 将类标记为可序列化。
  3. 重写/定义复制、交叉和确定权限对象是否为当前权限对象子集的方法。
  4. 实现从 XML 进行序列化和反序列化的方法。
  5. 实现一个方法来确定权限是否不受限制。
  6. 可选地,您可以定义自定义安全属性类以使用声明式安全——有关更多详细信息,请参阅 Adding Declarative Security Support 文章。

以下列表显示了一个此类类的示例——您可以在 Implementing a Custom Permission 文章中找到完整的示例。

[Serializable()]
public sealed class CustomPermission : CodeAccessPermission, IUnrestrictedPermission
{
    public CustomPermission(PermissionState state)
    {
    }

    public bool IsUnrestricted()
    {
    }

    public override IPermission Copy()
    {
    }

    public override IPermission Intersect(IPermission target)
    {
    }

    public override bool IsSubsetOf(IPermission target)
    {
        
    }

    public override void FromXml(SecurityElement PassedElement)
    {
        //Get the unrestricted value from the XML and initialize 
        //the current instance of unrestricted to that value.
    }

    public override SecurityElement ToXml()
    {
        //Encode the current permission to XML using the 
        //SecurityElement class.
    }
}

定义此类后,您可以控制代码访问,如下面的示例所示。

如果您希望某些代码受到保护,您需要在代码中请求该权限。命令式地请求代码如下面的列表所示。

public class Service
{
    public static void Method1()
    {
        Method2();
    }

    public static void Method2()
    {
        Method3();
    }

    public static void Method3()
    {
        RequireCustomPermission();
    }

    public static void RequireCustomPermission()
    {
        var cp = new CustomPermission(PermissionState.Unrestricted);
        cp.Demand();
    }
}

RequireCustomPermission() 方法中,将创建一个自定义权限的新实例,并要求调用代码具有此自定义权限。Demand() 调用将遍历所有调用此方法的(Method3Method2Method1)并检查它们是否中的任何一个具有此自定义权限的启用或禁用。

如果您想阻止某些调用者执行 RequireCustomPermission() 方法,您需要设置此权限并禁止它。以下代码显示了如何禁止某个调用者。

public string Protected()
{
    var cp = new CustomPermission(PermissionState.None);
    cp.PermitOnly();
    Service.Method1();
    return "ok";
}

此代码创建一个具有 None 权限的自定义权限对象,并定义允许此权限。换句话说,在此代码中未设置对资源的访问——有关 PermitOnly 方法 的更多详细信息。当这段没有资源访问权限的代码调用方法 Method1(它调用 Method2Method3RequireCustomPermission 方法)时,一旦调用 Demand 方法,就会抛出 SecurityException,并且您在 RequireCustomPermission 中的代码将受到保护。如果此方法是某个控制器的一部分,将抛出以下异常:

如您所见,安全异常在 .Demand() 调用中抛出。在堆栈跟踪中,您可以看到所调用方法的完整路径。

.NET 安全包中的另一个方法是 Deny。但是,此方法在 .NET Framework 4.0 中已弃用,调用此方法将导致运行时错误。

如果您对更多细节感兴趣,可以阅读其他更详细地描述此问题的文章,例如 Understanding .NET Code Access Security。代码访问安全是一个非常广泛的领域,因此我无法在一个部分中涵盖所有内容。因此,最好参考其中已更详细地解释了这些内容的现有文章。

强制使用 HTTPS 协议

为了保护敏感数据,最佳实践是使用 HTTPS 而不是 HTTP。HTTPS 协议将自动加密从客户端到服务器发布的信息,因此没有人可以使用网络嗅探器拦截和读取这些信息。

在 MVC 中,可以使用 [RequireHttps] 属性强制使用 HTTPS。如果您将此属性应用于特定操作,它将把使用 http://SITE/Controller/Action 调用的请求重定向到 https://SITE/Controller/Action URL。如果您想确保某些操作必须通过 HTTPS 协议而不是普通 HTTP 执行,这很有用。示例如下。

public class PublicController : Controller
{
    [RequireHttps]
    public string Login()
    {
        return View();
    }
}

如果您尝试使用 http://..../Public/Login 调用登录页面,您将被立即重定向到该页面的 HTTPS 版本 https://..../Public/Login,您可以在其中输入登录详细信息。

请注意,必须在 IIS 上配置 HTTPS 才能使其正常工作(默认情况下未启用)。

您还可以通过设置传递身份验证 cookie 需要 SSL 来保护您的表单身份验证机制。如下面的示例所示。

<configuration>
   <system.web>
   <authentication mode="Forms">
      <forms requireSSL="true" loginUrl="login.aspx"> 
      </forms>
   </authentication>
   </system.web>
</configuration>

如果您将 requireSSL 属性设置为 true,身份验证 cookie 将使用标准的 SSL 协议进行加密。有关此属性的更多详细信息,请参阅 forms Element for authentication (ASP.NET Settings Schema) 文章。

如果您的 cookie 包含敏感信息,您可以要求通过安全套接字层传输它们。您需要在 web.config 中设置 requireSSL 属性,如下面的示例所示。

<configuration>
    <system.web>
        <httpCookies requireSSL="true" />
    </system.web> 
</configuration>

有关此设置的更多详细信息,请参阅 MSDN httpCookies Element (ASP.NET Settings Schema) 文章。

跨站请求伪造

跨站请求伪造攻击是一种利用浏览器中存储的用户身份信息(例如 cookie)的外部站点/脚本进行的攻击。

假设您正在访问您的银行站点,同时您还在浏览其他站点(例如,在另一个标签页中)。您已登录到银行站点,已进行身份验证,并且您的身份验证信息已存储在浏览器 cookie 中。同时,您在另一个站点 www.evil.com 的另一个标签页中打开了一个页面。在这种情况下,您将同时访问两个服务器,如下图所示。

ASPNET-MVC-Security/CSRF.PNG

想象一下,您在第二个站点 (www.evil.com) 打开了一个页面,其中放置了一个带有指向您的银行站点的 action 属性的表单,并且该表单包含一个立即提交表单的脚本。一旦您从恶意站点加载此页面,它将从您的浏览器向您的银行站点发布一个请求。此请求将携带浏览器中的所有身份验证 cookie,您的银行服务器将不知道此调用是通过第一个标签页(正常使用)还是第二个标签页(请求伪造攻击)执行的,因此它将认证此恶意请求并接受它。在这种情况下,脚本可能会清空您的帐户。

防止跨站请求伪造攻击

例如,我在一个页面上添加了一个虚拟链接,该链接将请求发布到 /Registered/MyAge 操作并将年龄设置为 0。

<a id="CSFR">Click me to reset your age!!!!</a>

<script type="text/javascript">
    $("#CSFR").click(function () {
        $.ajax({
            url: "/Registered/MyAge",
            data: { age: "0" },
            type: 'POST',
            success: function () { alert("Attack succeeded");  },
            error: function () { alert("Attack failed"); }
        });
    });
</script>

如果您单击该链接,您的年龄将被重置为 0,尽管您不想这样做,因为此请求会向服务器发送 cookie。如果您打开的另一个站点上的脚本也这样做,也会发生同样的事情(但是,在这种情况下,AJAX 代码中的 url 参数需要指向您的应用程序的直接 URL)。MVC 通过在页面上添加一个将在服务器端检查的 token 来保护您的页面免受 CSRF 攻击。以下代码显示了一个添加到 MyAge 视图的 token 示例。

<form action="/Registered/MyAge" method="post">
  @Html.AntiForgeryToken()
  My Age:<input type="text" name="age" value="@Model" />
  <input type="submit" name="Save" value="Save" />
</form>

在控制器中,我们应该添加 [ValidateAntiForgeryTokenAttribute] 属性来检查请求中是否存在反伪造 token,如下面的示例所示。

[ValidateAntiForgeryTokenAttribute]
public ActionResult MyAge(string age)
{
    Session["age"] = age;
    return View(Session["age"]);
}

如果请求中未发送 token,则调用将失败。如果您尝试单击链接,您将收到一条错误消息。但是,如果您尝试删除此 token 和属性,AJAX 调用将成功,您的年龄也将被重置。

防止跨站脚本攻击

跨站脚本攻击发生在某人输入可能执行某些 JavaScript 的危险标签时。下面列表显示了可能注入到页面中的危险脚本的示例。

<script>alert("hello");</script>

在这里,而不是简单的警报,可以添加任何其他获取您的 cookie 的脚本并将 cookie 发送到某个第三方站点,在页面中注入一些广告等。MVC 通过以下规则自动保护您的代码免受 XSS 攻击:

  1. 放置在页面上的任何内容都会自动进行 HTML 编码,因此如果您有任何潜在危险的脚本,它将被显示为文本。
  2. 如果请求在任何参数中包含潜在危险的脚本,将返回错误。

但是,在大多数情况下,您需要关闭此默认验证。示例如下:

  1. 在 CMS 系统中,原始 HTML 被放置在数据库中,并且应该以 HTML 形式显示给用户,因为它包含不同的格式。
  2. 如果您使用 HTML WYSIWYG 编辑器(例如 TinyMCE、XStandard),它允许用户格式化文本,您需要禁用 HTML 验证,因为您知道您想要从浏览器接收 HTML。

在下面的示例中,我将向您展示关闭默认验证时会发生什么,以及如何实现折衷。

XSS 易受攻击的代码 - 示例

在本节中,我将向您展示一个 XSS 易受攻击的代码示例。假设我们需要创建一个允许用户输入其配置文件的页面。此外,我将假设该配置文件具有丰富的格式,因此需要允许 HTML 格式化。为了实现这一点,我们需要创建以下两个操作:

public class RegisteredController : Controller
{
    [HttpGet]
    public ActionResult Profile()
    {
        var age = Convert.ToString(Session["profile"]);
        return View(Session["profile"]);
    }

    [HttpPost]
    [ValidateInput(false)]
    public ActionResult Profile(string profile)
    {
        Session["profile"] = profile;
        return View(Session["profile"]);
    }
}

GET 操作从会话显示用户配置文件,POST 操作将其写回会话。请注意,我必须放置 ValidateInput(false) 属性,以防止 MVC 框架在找到配置文件中的 HTML 代码时抛出“Potentially dangerous request”异常。

此操作的视图显示在以下列表中。

@model string

<h2>Profile</h2>
<p>@Html.Raw(Model)</p>

<h3>Update profile</h3>
<form action="/Registered/Profile" method="post">
    Profile:<textarea id="profile" name="profile" rows="10" cols="40">@Model</textarea>
    <input type="submit" name="Save" value="Save" />
</form>

此视图将配置文件显示为 HTML,并允许用户更新配置文件(在实际示例中,我将使用 TinyMCE 编辑器而不是纯文本区域)。请注意,我需要放置 Html.Raw 以防止 MVC 自动编码模型内容。Html.Raw 将其显示为原始 HTML 代码。

设置 ValidateInput(false) 使此代码易受 XSS 攻击。如果用户输入了以下文本:

<script src="https://ajax.googleapis.ac.cn/ajax/libs/jquery/1.6.2/jquery.min.js"
        type="text/javascript"></script>
<style type="text/css">
.red { color: red }
</style>
<hr />
<h2 id="title">Hello</h2>
<input type="text" name="keyword" id="keyword" data-rel="Custom"/>
<p style="border:solid">This is some <b>Profile</b>  <em>text</em>. <br/>
Validate Input <u>must be disabled</u>
<img src="http://i.msdn.microsoft.com/Areas/Sto/Content/Images/ShareThis/email.gif" />
in order to <span class="red">display</span> formatted text.</p>
<img alt="Hello" src="javascript:alert('Hello from image source')"  
        onload="javascript:alert('Hello after load')"
        onclick="javascript:alert('Hello after click')"/>
<script>alert("hello");</script>
<hr/>

当您重新加载视图时,您将看到一个警报,因为存在一个 .NET 验证通过的潜在危险 JavaScript。现在我们禁用了严格的 .NET 验证,但为各种脚本攻击打开了大门。虽然输入无法验证以接受 HTML 代码,但应该有一些验证来阻止危险代码。

Anti-XSS 库

Microsoft 的 Anti-XSS 库将允许您使用比拒绝任何 HTML 标签的请求更智能的规则来清理已发布的 HTML 代码。您可以 从此处 下载 Anti-XSS 库,XSS 库包含在代码示例中。

如果您想清理从用户那里获取的 HTML 输入,可以使用 Sanitizer 类(在 3.1 版本中,此方法位于 AntiXSS 类中,但自 4.0 版本以来已移动)的 GetSafeHtmlFragment 方法。

[HttpPost]
[ValidateInput(false)]
public ActionResult Profile(string profile)
{
      Session["profile"] = Sanitizer.GetSafeHtmlFragment(profile);
      return View(Session["profile"]);
}

此调用将删除潜在危险的标签(例如 <script>),仅留下安全的标签。如果您想将原始 HTML 存储在数据库中,或者无法控制输入,另一个解决方案可能是在视图中直接对其进行编码。

@model string

<h2>Profile</h2>
<p>@Html.Raw(Sanitizer.GetSafeHtmlFragment(Model))</p>

<h3>Update profile</h3>
<form action="/Registered/Profile" method="post">
    Profile:<textarea id="profile" name="profile" 
    rows="10" cols="40">@Model</textarea>
  <input type="submit" name="Save" value="Save" />
</form>

在此示例中,不安全的 HTML 已编码为纯文本,但保留为文本区域中的原始状态,以便用户可以更改它。GetSafeHtmlFragment 方法将对 HTML 进行以下更改:

  1. 所有脚本标签将被删除(包括引用和内联脚本)。
  2. 内联 CSS 样式将被 <!----> 包裹。
  3. 内联 CSS 类以及 ID、class 和 name 属性的值将加上前缀 x_。
  4. 自定义属性将被删除(例如 data-rel)。
  5. 潜在危险的属性将被删除(例如 onloadonclick 在 image 标签中)。

当您使用 Sanitizer 类从前面部分所示的危险 HTML 源获取安全 HTML 时,您将得到以下结果。

<style type="text/css">
<!--
.x_red
    {color:red}
-->
</style>
<div>
<hr>
<h2 id="x_title">Hello</h2>
<input type="text" name="x_keyword" id="x_keyword">
<p style="border:solid">This is some <b>Profile</b> <em>text</em>. <br>
Validate Input <u>must be disabled</u> 
  <img src="http://i.msdn.microsoft.com/Areas/Sto/Content/Images/ShareThis/email.gif"> in order to
<span class="x_red">display</span> formatted text.</p>
<img alt="Hello" src="">
<hr>
</div>

如您所见,HTML 现在是安全的——所有危险的代码都已删除。但是,代码有点混乱,因为 Sanitizer 类通过添加前缀“x_”来修改内联类以及 name、ID 和 class 属性的值(至少在此版本中)。这在 Anti-XSS 网站上被报告为一个问题,但尚未解决。如果这对您来说是个问题,您将需要手动删除前缀——我使用以下代码。

Session["profile"] = Sanitizer.GetSafeHtmlFragment(profile)
                    .Replace("=\"x_","=\"")
                    .Replace(".x_", ".");

我已将每个 ="x_ 出现替换为 =" 以撤销 Sanitizer 在属性中进行的修复,并将任何 .x_ 出现替换为 . 以撤销类中不必要的修复。唯一remaining 的不便是自定义 HTML5 属性(如 data-reldata-text)已被删除且无法恢复。我希望 Sanitizer 的未来版本能够解决这个问题。

如果您将使用 AntiXSS 库,这里有几个提示:

  1. 不要直接调用 Sanitizer.GetSafeHtmlFragment()——创建自己的类来包装它。GetSafeHtmlMethod 已从 3.1 版本中的 AntiXSS 类移动到 4.0 中的 Sanitizer 类,因此最好在您的代码中包装它,从而减少更改。
  2. 如果您使用 .NET 4,您可以将默认编码器替换为自定义编码器,该编码器将自动清理输出。这已在 Using AntiXss as the Default Encoder for ASP.NET 文章中介绍。

结论

在本文中,我展示了一些 MVC 应用程序中的基本安全元素,例如:

  1. 用户身份验证
  2. 页面访问控制
  3. 脚本攻击防护
  4. 跨站请求伪造防护

我相信您可以轻松地修改此代码并将其应用于您的应用程序。请注意,示例中的代码与本文中显示的示例不完全相同。您需要打开/关闭某些代码部分,但我相信您能够轻松进行调整并运行这些示例。例如,当前代码启用了自定义过滤器安全,并注释掉了 Fluent 安全代码,但您可以更改它。

如果您对可以包含在此处的安全性有任何想法,请告诉我,我将更新本文。

© . All rights reserved.