跳到内容

DotNet 安全备忘单

简介

本页面旨在为开发人员提供快速基本的 .NET 安全提示。

.NET 框架

.NET 框架是 Microsoft 用于企业开发的主要平台。它是 ASP.NET、Windows 桌面应用程序、Windows Communication Foundation 服务、SharePoint、Visual Studio Office 工具及其他技术的支持 API。

.NET 框架包含一系列 API,它们促进了高级类型系统的使用,管理数据、图形、网络、文件操作等等——基本上涵盖了在 Microsoft 生态系统中开发企业应用程序的绝大多数需求。它是一个几乎无处不在的库,在程序集级别上具有强名称和版本控制。

更新框架

.NET 框架通过 Windows Update 服务由 Microsoft 保持最新。开发人员通常不需要单独运行框架更新。Windows Update 可以通过Windows Update或 Windows 计算机上的 Windows Update 程序访问。

可以使用 NuGet 保持各个框架的最新。当 Visual Studio 提示更新时,将其纳入您的生命周期。

请记住,第三方库必须单独更新,并非所有库都使用 NuGet。例如,ELMAH 需要单独的更新工作。

安全公告

通过在以下存储库中选择“Watch”按钮来接收安全通知

.NET 通用指南

本节包含 .NET 应用程序的通用指南。这适用于所有 .NET 应用程序,包括 ASP.NET、WPF、WinForms 等。

OWASP Top 10 列出了当今世界最普遍和最危险的 Web 安全威胁,并每隔几年进行审查并更新最新的威胁数据。本备忘单的这一部分基于此列表。保护您的 Web 应用程序的方法应该是从下面的 A1 顶级威胁开始,然后向下进行;这将确保任何花在安全上的时间都能最有效地利用,首先解决顶级威胁,然后是较小的威胁。在涵盖了 Top 10 之后,通常建议评估其他威胁或进行专业的渗透测试。

A01 权限管理失效

弱账户管理

确保使用 HttpOnly 标志发送 Cookie,以防止客户端脚本访问 Cookie

CookieHttpOnly = true,

通过缩短会话超时和移除滑动过期来减少会话被盗的时间段

ExpireTimeSpan = TimeSpan.FromMinutes(60),
SlidingExpiration = false

有关完整启动代码片段的示例,请参见此处

确保在生产环境中通过 HTTPS 发送 Cookie。这应该在配置转换中强制执行

<httpCookies requireSSL="true" />
<authentication>
    <forms requireSSL="true" />
</authentication>

通过限制请求(请参阅下面的代码)来保护登录、注册和密码重置方法免受暴力破解攻击。还可以考虑使用 ReCaptcha。

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[AllowXRequestsEveryXSecondsAttribute(Name = "LogOn",
Message = "You have performed this action more than {x} times in the last {n} seconds.",
Requests = 3, Seconds = 60)]
public async Task<ActionResult> LogOn(LogOnViewModel model, string returnUrl)

切勿:自行实现身份验证或会话管理。请使用 .NET 提供的功能。

切勿:在登录、注册或密码重置时告知用户帐户是否存在。应说“用户名或密码不正确”之类的消息,或者“如果此帐户存在,重置令牌将发送到注册的电子邮件地址”。这可以防止帐户枚举。

无论帐户是否存在,给用户的反馈在内容和行为上都应该相同。例如,如果帐户真实存在时响应时间延长 50%,则可以猜测并测试成员资格信息。

缺少功能级别访问控制

务必:在所有对外暴露的端点上授权用户。.NET 框架有多种方式来授权用户,可以在方法级别使用

[Authorize(Roles = "Admin")]
[HttpGet]
public ActionResult Index(int page = 1)

或者更好,在控制器级别使用

[Authorize]
public class UserController

您还可以使用 .NET 中的身份功能在代码中检查角色:System.Web.Security.Roles.IsUserInRole(userName, roleName)

您可以在授权备忘单授权测试自动化备忘单中找到更多信息。

不安全的直接对象引用

当您有一个可以通过引用(在下面的示例中是id)访问的资源(对象)时,您需要确保用户被授权访问该资源。

// Insecure
public ActionResult Edit(int id)
{
  var user = _context.Users.FirstOrDefault(e => e.Id == id);
  return View("Details", new UserViewModel(user);
}

// Secure
public ActionResult Edit(int id)
{
  var user = _context.Users.FirstOrDefault(e => e.Id == id);
  // Establish user has right to edit the details
  if (user.Id != _userIdentity.GetUserId())
  {
        HandleErrorInfo error = new HandleErrorInfo(
            new Exception("INFO: You do not have permission to edit these details"));
        return View("Error", error);
  }
  return View("Edit", new UserViewModel(user);
}

更多信息可以在不安全直接对象引用预防备忘单中找到。

A02 密码学失效

通用密码学指南

  • 永远不要编写自己的加密函数。
  • 尽可能避免编写任何加密代码。相反,尝试使用现有密钥管理解决方案或您的云提供商提供的密钥管理解决方案。有关更多信息,请参阅OWASP 密钥管理备忘单
  • 如果您无法使用现有的秘密管理解决方案,请尝试使用受信任且知名的实现库,而不是使用 .NET 内置的库,因为使用它们太容易出现加密错误。
  • 确保您的应用程序或协议能够轻松支持未来的加密算法更改。

哈希

务必:使用强大的哈希算法。

密码

务必:强制执行最低复杂度的密码,以抵御字典攻击;即使用完整字符集(数字、符号和字母)的更长密码以增加熵。

加密

务必:当个人身份数据需要恢复到其原始格式时,使用强大的加密算法,如 AES-512。

务必:比任何其他资产都更严格地保护加密密钥。有关存储静态加密密钥的更多信息,请参阅密钥管理备忘单

务必:为您的整个网站使用 TLS 1.2+。从 LetsEncrypt.org 获取免费证书并自动化续订。

切勿:允许使用 SSL,这已过时

务必:制定强 TLS 策略(请参阅SSL 最佳实践),尽可能使用 TLS 1.2+。然后使用SSL 测试TestSSL检查配置。

有关传输层保护的更多信息,请参见传输层安全备忘单

务必:确保响应头不泄露您的应用程序信息。请参阅HttpHeaders.csDionach StripHeaders,通过 web.configStartup.cs 禁用。

例如 Web.config

<system.web>
    <httpRuntime enableVersionHeader="false"/>
</system.web>
<system.webServer>
    <security>
        <requestFiltering removeServerHeader="true" />
    </security>
    <httpProtocol>
        <customHeaders>
            <add name="X-Content-Type-Options" value="nosniff" />
            <add name="X-Frame-Options" value="DENY" />
            <add name="X-Permitted-Cross-Domain-Policies" value="master-only"/>
            <add name="X-XSS-Protection" value="0"/>
            <remove name="X-Powered-By"/>
        </customHeaders>
    </httpProtocol>
</system.webServer>

例如 Startup.cs

app.UseHsts(hsts => hsts.MaxAge(365).IncludeSubdomains());
app.UseXContentTypeOptions();
app.UseReferrerPolicy(opts => opts.NoReferrer());
app.UseXXssProtection(options => options.FilterDisabled());
app.UseXfo(options => options.Deny());

app.UseCsp(opts => opts
 .BlockAllMixedContent()
 .StyleSources(s => s.Self())
 .StyleSources(s => s.UnsafeInline())
 .FontSources(s => s.Self())
 .FormActions(s => s.Self())
 .FrameAncestors(s => s.Self())
 .ImageSources(s => s.Self())
 .ScriptSources(s => s.Self())
 );

有关头部的更多信息,请访问OWASP 安全头部项目

存储加密

以下代码片段展示了使用 AES-GCM 执行数据加密/解密的示例。强烈建议让密码学专家审查您的最终设计和代码,因为即使最微小的错误也可能严重削弱您的加密。

该代码基于此处的示例:https://www.scottbrady91.com/c-sharp/aes-gcm-dotnet

这段代码的一些限制/陷阱

  • 它没有考虑密钥轮换或管理,这本身就是一个完整的主题。
  • 对于每次加密操作,即使使用相同的密钥,也务必使用不同的 nonce。
  • 密钥需要安全存储。
点击此处查看“AES-GCM 对称加密”代码片段。
// Code based on example from here:
// https://www.scottbrady91.com/c-sharp/aes-gcm-dotnet

public class AesGcmSimpleTest
{
    public static void Main()
    {

        // Key of 32 bytes / 256 bits for AES
        var key = new byte[32];
        RandomNumberGenerator.Fill(key);

        // MaxSize = 12 bytes / 96 bits and this size should always be used.
        var nonce = new byte[AesGcm.NonceByteSizes.MaxSize];
        RandomNumberGenerator.Fill(nonce);

        // Tag for authenticated encryption
        var tag = new byte[AesGcm.TagByteSizes.MaxSize];

        var message = "This message to be encrypted";
        Console.WriteLine(message);

        // Encrypt the message
        var cipherText = AesGcmSimple.Encrypt(message, nonce, out tag, key);
        Console.WriteLine(Convert.ToBase64String(cipherText));

        // Decrypt the message
        var message2 = AesGcmSimple.Decrypt(cipherText, nonce, tag, key);
        Console.WriteLine(message2);


    }
}


public static class AesGcmSimple
{

    public static byte[] Encrypt(string plaintext, byte[] nonce, out byte[] tag, byte[] key)
    {
        using(var aes = new AesGcm(key))
        {
            // Tag for authenticated encryption
            tag = new byte[AesGcm.TagByteSizes.MaxSize];

            // Create a byte array from the message to encrypt
            var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);

            // Ciphertext will be same length in bytes as plaintext
            var ciphertext = new byte[plaintextBytes.Length];

            // perform the actual encryption
            aes.Encrypt(nonce, plaintextBytes, ciphertext, tag);
            return ciphertext;
        }
    }

    public static string Decrypt(byte[] ciphertext, byte[] nonce, byte[] tag, byte[] key)
    {
        using(var aes = new AesGcm(key))
        {
            // Plaintext will be same length in bytes as Ciphertext
            var plaintextBytes = new byte[ciphertext.Length];

            // perform the actual decryption
            aes.Decrypt(nonce, ciphertext, tag, plaintextBytes);

            return Encoding.UTF8.GetString(plaintextBytes);
        }
    }
}

传输加密

以下代码片段展示了使用椭圆曲线/迪菲-赫尔曼 (ECDH) 结合 AES-GCM 在两个不同方之间执行数据加密/解密的示例,而无需在两方之间传输对称密钥。相反,双方交换公钥,然后可以使用 ECDH 生成一个共享密钥,该密钥可用于对称加密。

再次强调,强烈建议让密码学专家审查您的最终设计和代码,因为即使最微小的错误也可能严重削弱您的加密。

请注意,此代码示例依赖于上一节中的 AesGcmSimple 类。

这段代码的一些限制/陷阱

  • 它没有考虑密钥轮换或管理,这本身就是一个完整的主题。
  • 代码特意对每次加密操作强制使用新的 nonce,但这必须作为密文旁边的单独数据项进行管理。
  • 私钥需要安全存储。
  • 代码未考虑使用前对公钥的验证。
  • 总而言之,双方之间没有进行真实性验证。
点击此处查看“ECDH 非对称加密”代码片段。
public class ECDHSimpleTest
{
    public static void Main()
    {
        // Generate ECC key pair for Alice
        var alice = new ECDHSimple();
        byte[] alicePublicKey = alice.PublicKey;

        // Generate ECC key pair for Bob
        var bob = new ECDHSimple();
        byte[] bobPublicKey = bob.PublicKey;

        string plaintext = "Hello, Bob! How are you?";
        Console.WriteLine("Secret being sent from Alice to Bob: " + plaintext);

        // Note that a new nonce is generated with every encryption operation in line with
        // in line with the AES GCM security
        byte[] tag;
        byte[] nonce;
        var cipherText = alice.Encrypt(bobPublicKey, plaintext, out nonce, out tag);
        Console.WriteLine("Ciphertext, nonce, and tag being sent from Alice to Bob: " + Convert.ToBase64String(cipherText) + " " + Convert.ToBase64String(nonce) + " " + Convert.ToBase64String(tag));

        var decrypted = bob.Decrypt(alicePublicKey, cipherText, nonce, tag);
        Console.WriteLine("Secret received by Bob from Alice: " + decrypted);

        Console.WriteLine();

        string plaintext2 = "Hello, Alice! I'm good, how are you?";
        Console.WriteLine("Secret being sent from Bob to Alice: " + plaintext2);

        byte[] tag2;
        byte[] nonce2;
        var cipherText2 = bob.Encrypt(alicePublicKey, plaintext2, out nonce2, out tag2);
        Console.WriteLine("Ciphertext, nonce, and tag being sent from Bob to Alice: " + Convert.ToBase64String(cipherText2) + " " + Convert.ToBase64String(nonce2) + " " + Convert.ToBase64String(tag2));

        var decrypted2 = alice.Decrypt(bobPublicKey, cipherText2, nonce2, tag2);
        Console.WriteLine("Secret received by Alice from Bob: " + decrypted2);
    }
}


public class ECDHSimple
{

    private ECDiffieHellmanCng ecdh = new ECDiffieHellmanCng();

    public byte[] PublicKey
    {
        get
        {
            return ecdh.PublicKey.ToByteArray();
        }
    }

    public byte[] Encrypt(byte[] partnerPublicKey, string message, out byte[] nonce, out byte[] tag)
    {
        // Generate the AES Key and Nonce
        var aesKey = GenerateAESKey(partnerPublicKey);

        // Tag for authenticated encryption
        tag = new byte[AesGcm.TagByteSizes.MaxSize];

        // MaxSize = 12 bytes / 96 bits and this size should always be used.
        // A new nonce is generated with every encryption operation in line with
        // the AES GCM security model
        nonce = new byte[AesGcm.NonceByteSizes.MaxSize];
        RandomNumberGenerator.Fill(nonce);

        // return the encrypted value
        return AesGcmSimple.Encrypt(message, nonce, out tag, aesKey);
    }


    public string Decrypt(byte[] partnerPublicKey, byte[] ciphertext, byte[] nonce, byte[] tag)
    {
        // Generate the AES Key and Nonce
        var aesKey = GenerateAESKey(partnerPublicKey);

        // return the decrypted value
        return AesGcmSimple.Decrypt(ciphertext, nonce, tag, aesKey);
    }

    private byte[] GenerateAESKey(byte[] partnerPublicKey)
    {
        // Derive the secret based on this side's private key and the other side's public key
        byte[] secret = ecdh.DeriveKeyMaterial(CngKey.Import(partnerPublicKey, CngKeyBlobFormat.EccPublicBlob));

        byte[] aesKey = new byte[32]; // 256-bit AES key
        Array.Copy(secret, 0, aesKey, 0, 32); // Copy first 32 bytes as the key

        return aesKey;
    }
}

A03 注入

SQL 注入

务必:使用对象关系映射器 (ORM) 或存储过程是应对 SQL 注入漏洞最有效的方法。

务必:在必须使用直接 SQL 查询的情况下,使用参数化查询。更多信息可以在查询参数化备忘单中找到。

例如,使用 Entity Framework

var sql = @"Update [User] SET FirstName = @FirstName WHERE Id = @Id";
context.Database.ExecuteSqlCommand(
    sql,
    new SqlParameter("@FirstName", firstname),
    new SqlParameter("@Id", id));

切勿:在代码中的任何地方连接字符串并针对数据库执行它们(称为动态 SQL)。

注意:您仍可能在使用 ORM 或存储过程时无意中这样做,因此请检查所有地方。例如

string sql = "SELECT * FROM Users WHERE UserName='" + txtUser.Text + "' AND Password='"
                + txtPassword.Text + "'";
context.Database.ExecuteSqlCommand(sql); // SQL Injection vulnerability!

务必:实践最小权限原则——使用具有完成其工作所需的最小权限集的帐户连接到数据库,而不是数据库管理员帐户。

操作系统注入

关于操作系统注入的通用指南可以在操作系统命令注入防御备忘单中找到。

务必:使用 System.Diagnostics.Process.Start 调用底层操作系统函数。

例如

var process = new System.Diagnostics.Process();
var startInfo = new System.Diagnostics.ProcessStartInfo();
startInfo.FileName = "validatedCommand";
startInfo.Arguments = "validatedArg1 validatedArg2 validatedArg3";
process.StartInfo = startInfo;
process.Start();

切勿:假定此机制能够防御旨在跳出一个参数并篡改进程的另一个参数的恶意输入。这仍然是可能的。

务必:尽可能对所有用户提供输入使用允许列表验证。输入验证可以防止格式不正确的数据进入信息系统。有关更多信息,请参见输入验证备忘单

例如使用 IPAddress.TryParse 方法验证用户输入

//User input
string ipAddress = "127.0.0.1";

//check to make sure an ip address was provided
if (!string.IsNullOrEmpty(ipAddress))
{
 // Create an instance of IPAddress for the specified address string (in
 // dotted-quad, or colon-hexadecimal notation).
 if (IPAddress.TryParse(ipAddress, out var address))
 {
  // Display the address in standard notation.
  return address.ToString();
 }
 else
 {
  //ipAddress is not of type IPAddress
  ...
 }
    ...
}

务必:尽量只接受简单的字母数字字符。

切勿:假定您可以在不实际移除特殊字符的情况下对其进行清理。\'@ 的各种组合可能对清理尝试产生意想不到的影响。

切勿:依赖没有安全保证的方法。

例如,.NET Core 2.2 及更高版本和 .NET 5 及更高版本支持 ProcessStartInfo.ArgumentList,它执行一些字符转义,但该对象包含一份免责声明,指出它在处理不可信输入时不安全

务必:考虑替代通过命令行参数传递原始不可信参数的方法,例如使用 Base64 编码(这也将安全地编码任何特殊字符),然后在接收应用程序中解码参数。

LDAP 注入

几乎任何字符都可以在专有名称中使用。但是,某些字符必须使用反斜杠 \ 转义字符进行转义。有关 Active Directory 中应转义哪些字符的表格,请参见LDAP 注入防御备忘单

注意:空格字符只有当它是一个组件名称(例如通用名称)的开头或结尾字符时才需要转义。嵌入式空格不应转义。

更多信息可以在LDAP 注入防御备忘单中找到。

A04 不安全设计

不安全设计是指应用程序或系统设计中的安全缺陷。这与 OWASP Top 10 列表中其他指的是实现缺陷的项目不同。因此,安全设计的主题与特定技术或语言无关,因此不在本备忘单的讨论范围之内。有关更多信息,请参见安全产品设计备忘单

A05 安全配置错误

调试和堆栈跟踪

确保在生产环境中关闭调试和跟踪。这可以通过 web.config 转换强制执行

<compilation xdt:Transform="RemoveAttributes(debug)" />
<trace enabled="false" xdt:Transform="Replace"/>

切勿:使用默认密码

务必:将通过 HTTP 发出的请求重定向到 HTTPS

例如,Global.asax.cs

protected void Application_BeginRequest()
{
    #if !DEBUG
    // SECURE: Ensure any request is returned over SSL/TLS in production
    if (!Request.IsLocal && !Context.Request.IsSecureConnection) {
        var redirect = Context.Request.Url.ToString()
                        .ToLower(CultureInfo.CurrentCulture)
                        .Replace("http:", "https:");
        Response.Redirect(redirect);
    }
    #endif
}

例如,Configure() 中的 Startup.cs

  app.UseHttpsRedirection();

跨站请求伪造

切勿:在未验证反伪造令牌的情况下发送敏感数据(.NET / .NET Core)。

务必:在每个 POST/PUT 请求中发送反伪造令牌

使用 .NET 框架
using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm",
                        @class = "pull-right" }))
{
    @Html.AntiForgeryToken()
    <ul class="nav nav-pills">
        <li role="presentation">
        Logged on as @User.Identity.Name
        </li>
        <li role="presentation">
        <a href="javascript:document.getElementById('logoutForm').submit()">Log off</a>
        </li>
    </ul>
}

然后,在方法级别或最好在控制器级别验证它

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()

确保在注销时彻底移除令牌以使其失效。

/// <summary>
/// SECURE: Remove any remaining cookies including Anti-CSRF cookie
/// </summary>
public void RemoveAntiForgeryCookie(Controller controller)
{
    string[] allCookies = controller.Request.Cookies.AllKeys;
    foreach (string cookie in allCookies)
    {
        if (controller.Response.Cookies[cookie] != null &&
            cookie == "__RequestVerificationToken")
        {
            controller.Response.Cookies[cookie].Expires = DateTime.Now.AddDays(-1);
        }
    }
}
使用 .NET Core 2.0 或更高版本

从 .NET Core 2.0 开始,可以自动生成和验证反伪造令牌

如果您正在使用标签助手(tag-helpers),这是大多数 Web 项目模板的默认设置,那么所有表单将自动发送反伪造令牌。您可以通过检查您的主 _ViewImports.cshtml 文件是否包含以下内容来确认标签助手是否已启用

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

IHtmlHelper.BeginForm 也会自动发送反伪造令牌。

如果您没有使用标签助手或 IHtmlHelper.BeginForm,您必须在表单上使用必要的辅助器,如下所示

<form action="RelevantAction" >
@Html.AntiForgeryToken()
</form>

若要自动验证除 GET、HEAD、OPTIONS 和 TRACE 之外的所有请求,您需要在 Startup.cs 中添加一个带有 AutoValidateAntiforgeryToken 属性的全局操作过滤器,如以下文章中所述

services.AddMvc(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

如果您需要在控制器上的特定方法(对于 MVC 控制器)或父类(对于 Razor 页面)禁用属性验证,您可以向其添加 IgnoreAntiforgeryToken 属性

[IgnoreAntiforgeryToken]
[HttpDelete]
public IActionResult Delete()
[IgnoreAntiforgeryToken]
public class UnsafeModel : PageModel

如果您还需要验证 GET、HEAD、OPTIONS 和 TRACE 请求的令牌,您可以将 ValidateAntiforgeryToken 属性添加到控制器方法(对于 MVC 控制器)或父类(对于 Razor 页面)

[HttpGet]
[ValidateAntiforgeryToken]
public IActionResult DoSomethingDangerous()
[HttpGet]
[ValidateAntiforgeryToken]
public class SafeModel : PageModel

如果您不能使用全局动作过滤器,请将 AutoValidateAntiforgeryToken 属性添加到您的控制器类或 Razor 页面模型中

[AutoValidateAntiforgeryToken]
public class UserController
[AutoValidateAntiforgeryToken]
public class SafeModel : PageModel
将 .Net Core 或 .NET 框架与 AJAX 结合使用

您需要将反伪造令牌附加到 AJAX 请求。

如果您在 ASP.NET Core MVC 视图中使用 jQuery,可以通过此代码片段实现

@inject  Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgeryProvider
$.ajax(
{
    type: "POST",
    url: '@Url.Action("Action", "Controller")',
    contentType: "application/x-www-form-urlencoded; charset=utf-8",
    data: {
        id: id,
        '__RequestVerificationToken': '@antiforgeryProvider.GetAndStoreTokens(this.Context).RequestToken'
    }
})

如果您正在使用 .NET Framework,您可以在此处找到一些代码片段。

更多信息可以在跨站请求伪造预防备忘单中找到。

A06 易受攻击和过时组件

务必:保持 .NET 框架更新到最新的补丁

务必:保持您的 NuGet 包最新

务必:将 OWASP 依赖项检查器作为构建过程的一部分对您的应用程序运行,并处理任何高级别或关键级别的漏洞。

务必:在 CI/CD 流水线中包含 SCA(软件组成分析)工具,以确保检测并处理您的依赖项中任何新的漏洞。

A07 身份识别与认证失效

务必:使用 ASP.NET Core Identity。ASP.NET Core Identity 框架默认配置良好,它使用安全的密码哈希和单独的盐。Identity 使用 PBKDF2 哈希函数进行密码处理,并为每个用户生成一个随机盐。

务必:设置安全的密码策略

例如 ASP.NET Core Identity

//Startup.cs
services.Configure<IdentityOptions>(options =>
{
 // Password settings
 options.Password.RequireDigit = true;
 options.Password.RequiredLength = 8;
 options.Password.RequireNonAlphanumeric = true;
 options.Password.RequireUppercase = true;
 options.Password.RequireLowercase = true;
 options.Password.RequiredUniqueChars = 6;

 options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
 options.Lockout.MaxFailedAccessAttempts = 3;

 options.SignIn.RequireConfirmedEmail = true;

 options.User.RequireUniqueEmail = true;
});

务必:设置 Cookie 策略

例如

//Startup.cs
services.ConfigureApplicationCookie(options =>
{
 options.Cookie.HttpOnly = true;
 options.Cookie.Expiration = TimeSpan.FromHours(1)
 options.SlidingExpiration = true;
});

A08 软件和数据完整性失效

务必:对程序集和可执行文件进行数字签名

务必:使用 Nuget 包签名

务必:审查代码和配置更改,以避免引入恶意代码或依赖项

切勿:通过网络发送未签名或未加密的序列化对象

务必:对从网络接收到的序列化对象执行完整性检查或验证数字签名

切勿:使用 BinaryFormatter 类型,它很危险且不建议用于数据处理。.NET 提供了几种内置的序列化程序,可以安全地处理不可信数据

  • XmlSerializerDataContractSerializer 用于将对象图序列化为 XML 和从 XML 反序列化。请勿将 DataContractSerializerNetDataContractSerializer 混淆。
  • BinaryReaderBinaryWriter 用于 XML 和 JSON。
  • System.Text.Json API 用于将对象图序列化为 JSON。

A09 安全日志记录和监控失效

务必:确保所有登录、访问控制和服务器端输入验证失败都记录了足够的用户上下文,以便识别可疑或恶意帐户。

务必:建立有效的监控和警报机制,以便及时检测和响应可疑活动。

切勿:记录通用错误消息,例如:csharp Log.Error("Error was thrown");。相反,应记录堆栈跟踪、错误消息以及导致错误的用户 ID。

切勿:记录敏感数据,例如用户密码。

日志记录

要收集哪些日志以及有关日志记录的更多信息,请参阅日志记录备忘单

.NET Core 附带了一个 LoggerFactory,它位于 Microsoft.Extensions.Logging 中。有关 ILogger 的更多信息可以在此处找到。

以下是如何从 Startup.cs 记录所有错误,以便在抛出错误时将其记录下来

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        _isDevelopment = true;
        app.UseDeveloperExceptionPage();
    }

    //Log all errors in the application
    app.UseExceptionHandler(errorApp =>
    {
        errorApp.Run(async context =>
        {
            var errorFeature = context.Features.Get<IExceptionHandlerFeature>();
            var exception = errorFeature.Error;

            Log.Error(String.Format("Stacktrace of error: {0}",exception.StackTrace.ToString()));
        });
    });

    app.UseAuthentication();
    app.UseMvc();
 }
}

例如,注入到类构造函数中,这样可以简化单元测试的编写。如果类的实例将使用依赖注入创建(例如 MVC 控制器),则建议这样做。下面的示例展示了所有不成功登录尝试的日志记录。

public class AccountsController : Controller
{
        private ILogger _Logger;

        public AccountsController(ILogger logger)
        {
            _Logger = logger;
        }

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginViewModel model)
        {
            if (ModelState.IsValid)
            {
                var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
                if (result.Succeeded)
                {
                    //Log all successful log in attempts
                    Log.Information(String.Format("User: {0}, Successfully Logged in", model.Email));
                    //Code for successful login
                    //...
                }
                else
                {
                    //Log all incorrect log in attempts
                    Log.Information(String.Format("User: {0}, Incorrect Password", model.Email));
                }
             }
            ...
        }

监控

监控使我们能够通过关键性能指标验证运行中系统的性能和健康状况。

在 .NET 中,添加监控功能的一个好选择是 Application Insights

有关日志记录和监控的更多信息,请参见此处

A10 服务器端请求伪造 (SSRF)

务必:在使用用户输入发出请求之前对其进行验证和清理

务必:使用允许的协议和域白名单

务必:使用 IPAddress.TryParse()Uri.CheckHostName() 来确保 IP 地址和域名有效

切勿:跟随 HTTP 重定向

切勿:将原始 HTTP 响应转发给用户

更多信息请参见服务器端请求伪造预防备忘单

OWASP 2013 & 2017

以下是 2013 年或 2017 年 OWASP Top 10 列表中包含但未包含在 2021 年列表中的漏洞。这些漏洞仍然相关,但未包含在 2021 年列表中,因为它们已变得不那么普遍。

A04:2017 XML 外部实体 (XXE)

XXE 攻击发生在 XML 解析器未正确处理包含 XML 有效载荷的文档类型中外部实体声明的用户输入时。

本文讨论了 .NET 最常见的 XML 处理选项。

请参阅 XXE 备忘单以获取有关防止 XXE 和其他 XML 拒绝服务攻击的更详细信息。

A07:2017 跨站脚本 (XSS)

切勿:信任用户发送给您的任何数据。优先使用白名单(始终安全)而非黑名单。

使用 MVC3 您可以对所有 HTML 内容进行编码。要正确编码所有内容,无论是 HTML、JavaScript、CSS、LDAP 等,请使用 Microsoft AntiXSS 库

Install-Package AntiXSS

然后进行配置

<system.web>
<httpRuntime targetFramework="4.5"
enableVersionHeader="false"
encoderType="Microsoft.Security.Application.AntiXssEncoder, AntiXssLibrary"
maxRequestLength="4096" />

切勿:使用 [AllowHTML] 属性或辅助类 @Html.Raw,除非您绝对确定您写入浏览器中的内容是安全的并且已正确转义。

务必:启用内容安全策略。这将防止您的页面访问它们不应该访问的资产(例如恶意脚本)

<system.webServer>
    <httpProtocol>
        <customHeaders>
            <add name="Content-Security-Policy"
                value="default-src 'none'; style-src 'self'; img-src 'self';
                font-src 'self'; script-src 'self'" />

更多信息可以在跨站脚本预防备忘单中找到。

A08:2017 不安全的反序列化

切勿:接受来自不可信源的序列化对象

务必:验证用户输入

恶意用户能够使用诸如 cookie 之类的对象来插入恶意信息以更改用户角色。在某些情况下,黑客能够通过使用先前会话中已存在或缓存的密码哈希,将他们的权限提升到管理员权限。

务必:防止对域对象进行反序列化

务必:以受限的访问权限运行反序列化代码。如果反序列化的恶意对象试图启动系统进程或访问服务器或主机操作系统内的资源,它将被拒绝访问并会引发权限标志,以便系统管理员能够了解服务器上的任何异常活动。

有关不安全反序列化的更多信息,请参见反序列化备忘单

A10:2013 未经验证的重定向和转发

MVC 3 模板中引入了对此的保护。代码如下

public async Task<ActionResult> LogOn(LogOnViewModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        var logonResult = await _userManager.TryLogOnAsync(model.UserName, model.Password);
        if (logonResult.Success)
        {
            await _userManager.LogOnAsync(logonResult.UserName, model.RememberMe);  
            return RedirectToLocal(returnUrl);
...
private ActionResult RedirectToLocal(string returnUrl)
{
    if (Url.IsLocalUrl(returnUrl))
    {
        return Redirect(returnUrl);
    }
    else
    {
        return RedirectToAction("Landing", "Account");
    }
}

其他建议

  • 防止点击劫持和中间人攻击捕获初始非 TLS 请求:设置 X-Frame-OptionsStrict-Transport-Security (HSTS) 头部。完整详情请参见此处
  • 防止从未访问过您网站的用户受到中间人攻击。注册HSTS preload
  • 对 Web API 服务进行安全测试和分析。它们隐藏在 MVC 站点内部,但却是攻击者可以找到的公共站点部分。所有 MVC 指南以及大部分 WCF 指南也适用于 Web API。
  • 另请参见未经验证的重定向和转发备忘单

示例项目

有关上述所有内容以及集成到具有增强安全基线的示例 MVC5 应用程序中的代码示例的更多信息,请访问Security Essentials Baseline 项目

特定主题指南

本节包含 .NET 中特定主题的指南。

配置和部署

  • 锁定配置文件。
    • 移除所有未使用的配置方面。
    • 使用 aspnet_regiis -pe命令行帮助)加密 web.config 中的敏感部分。
  • 对于 ClickOnce 应用程序,.NET Framework 应升级到最新版本,以确保支持 TLS 1.2 或更高版本。

数据访问

  • 毫无例外地,对所有数据访问使用参数化 SQL 命令。
  • 切勿将 SqlCommand 与由连接的 SQL 字符串组成的字符串参数一起使用。
  • 列出来自用户的允许值。使用枚举、TryParse 或查找值以确保来自用户的数据符合预期。
    • 枚举仍然容易受到意外值的影响,因为 .NET 只验证是否成功转换为底层数据类型(默认情况下为整数)。Enum.IsDefined 可以验证输入值在定义常量列表中是否有效。
  • 在您选择的数据库中设置数据库用户时,应用最小权限原则。数据库用户应只能访问对其用例有意义的项目。
  • 使用 Entity Framework 是一种非常有效的 SQL 注入防御机制。请记住,在 Entity Framework 中构建自己的即席查询与普通 SQL 查询一样容易受到 SQLi 的攻击
  • 使用 SQL Server 时,优先使用集成身份验证而非SQL 身份验证
  • 尽可能对敏感数据使用始终加密(SQL Server 2016+ 和 Azure SQL)

ASP.NET Web 窗体指南

ASP.NET Web Forms 是 .NET Framework 原始的基于浏览器的应用程序开发 API,至今仍是 Web 应用程序开发最常见的企业平台。

  • 始终使用 HTTPS
  • 在 web.config 中对 cookie 和表单元素启用 requireSSL,对 cookie 启用 HttpOnly
  • 实现 customErrors
  • 确保跟踪已关闭。
  • 虽然 ViewState 并非总是适合 Web 开发,但使用它可以提供 CSRF 缓解。要使 ViewState 防御 CSRF 攻击,您需要设置 ViewStateUserKey
protected override OnInit(EventArgs e) {
    base.OnInit(e);
    ViewStateUserKey = Session.SessionID;
}

如果您不使用 Viewstate,那么请查看 ASP.NET Web 窗体默认模板的主页,了解如何使用双提交 cookie 实现手动反 CSRF 令牌。

private const string AntiXsrfTokenKey = "__AntiXsrfToken";
private const string AntiXsrfUserNameKey = "__AntiXsrfUserName";
private string _antiXsrfTokenValue;
protected void Page_Init(object sender, EventArgs e)
{
    // The code below helps to protect against XSRF attacks
    var requestCookie = Request.Cookies[AntiXsrfTokenKey];
    Guid requestCookieGuidValue;
    if (requestCookie != null && Guid.TryParse(requestCookie.Value, out requestCookieGuidValue))
    {
       // Use the Anti-XSRF token from the cookie
       _antiXsrfTokenValue = requestCookie.Value;
       Page.ViewStateUserKey = _antiXsrfTokenValue;
    }
    else
    {
       // Generate a new Anti-XSRF token and save to the cookie
       _antiXsrfTokenValue = Guid.NewGuid().ToString("N");
       Page.ViewStateUserKey = _antiXsrfTokenValue;
       var responseCookie = new HttpCookie(AntiXsrfTokenKey)
       {
          HttpOnly = true,
          Value = _antiXsrfTokenValue
       };
       if (FormsAuthentication.RequireSSL && Request.IsSecureConnection)
       {
          responseCookie.Secure = true;
       }
       Response.Cookies.Set(responseCookie);
    }
    Page.PreLoad += master_Page_PreLoad;
}
protected void master_Page_PreLoad(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
       // Set Anti-XSRF token
       ViewState[AntiXsrfTokenKey] = Page.ViewStateUserKey;
       ViewState[AntiXsrfUserNameKey] = Context.User.Identity.Name ?? String.Empty;
    }
    else
    {
       // Validate the Anti-XSRF token
       if ((string)ViewState[AntiXsrfTokenKey] != _antiXsrfTokenValue ||
          (string)ViewState[AntiXsrfUserNameKey] != (Context.User.Identity.Name ?? String.Empty))
       {
          throw new InvalidOperationException("Validation of Anti-XSRF token failed.");
       }
    }
}
  • 考虑在 IIS 中使用 HSTS。有关步骤,请参见此处
  • 这是一个推荐的 web.config 设置,它处理 HSTS 和其他方面。
<?xml version="1.0" encoding="UTF-8"?>
 <configuration>
   <system.web>
     <httpRuntime enableVersionHeader="false"/>
   </system.web>
   <system.webServer>
     <security>
       <requestFiltering removeServerHeader="true" />
     </security>
     <staticContent>
       <clientCache cacheControlCustom="public"
            cacheControlMode="UseMaxAge"
            cacheControlMaxAge="1.00:00:00"
            setEtag="true" />
     </staticContent>
     <httpProtocol>
       <customHeaders>
         <add name="Content-Security-Policy"
            value="default-src 'none'; style-src 'self'; img-src 'self'; font-src 'self'" />
         <add name="X-Content-Type-Options" value="NOSNIFF" />
         <add name="X-Frame-Options" value="DENY" />
         <add name="X-Permitted-Cross-Domain-Policies" value="master-only"/>
         <add name="X-XSS-Protection" value="0"/>
         <remove name="X-Powered-By"/>
       </customHeaders>
     </httpProtocol>
     <rewrite>
       <rules>
         <rule name="Redirect to https">
           <match url="(.*)"/>
           <conditions>
             <add input="{HTTPS}" pattern="Off"/>
             <add input="{REQUEST_METHOD}" pattern="^get$|^head$" />
           </conditions>
           <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent"/>
         </rule>
       </rules>
       <outboundRules>
         <rule name="Add HSTS Header" enabled="true">
           <match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
           <conditions>
             <add input="{HTTPS}" pattern="on" ignoreCase="true" />
           </conditions>
           <action type="Rewrite" value="max-age=15768000" />
         </rule>
       </outboundRules>
     </rewrite>
   </system.webServer>
 </configuration>
  • 通过在 Machine.config 文件中添加以下行来删除版本头
<httpRuntime enableVersionHeader="false" />
  • 还要使用代码中的 HttpContext 类删除服务器头。
HttpContext.Current.Response.Headers.Remove("Server");

HTTP 验证和编码

  • 请勿在 web.config 或页面设置中禁用 validateRequest。此值在 ASP.NET 中启用有限的 XSS 保护,应保持不变,因为它提供部分跨站脚本预防。建议在内置保护的基础上进行完整的请求验证。
  • .NET Framework 4.5 版包含了 AntiXssEncoder 库,该库拥有全面的输入编码库用于预防 XSS。请使用它。
  • 只要接受用户输入,就列出允许的值。
  • 使用 Uri.IsWellFormedUriString 验证 URI 的格式。

表单认证

  • 尽可能使用 cookie 进行持久化。无 cookie 认证将默认使用 UseDeviceProfile
  • 不要信任请求的 URI 用于会话或授权的持久化。它很容易被伪造。
  • 将 Forms Authentication 的超时时间从默认的 20 分钟缩短到适用于您应用程序的最短时间。如果使用了 slidingExpiration,则每次请求后此超时都会重置,因此活跃用户不会受到影响。
  • 如果未使用 HTTPS,则应禁用 slidingExpiration。即使使用 HTTPS,也应考虑禁用 slidingExpiration
  • 始终实施适当的访问控制。
    • 将用户提供的用户名与 User.Identity.Name 进行比较。
    • 根据 User.Identity.IsInRole 检查角色。
  • 使用 ASP.NET 成员资格提供程序和角色提供程序,但请检查密码存储。默认存储使用 SHA-1 单次迭代哈希密码,这相当弱。ASP.NET MVC4 模板使用 ASP.NET Identity 而不是 ASP.NET 成员资格,并且 ASP.NET Identity 默认使用 PBKDF2,这更好。有关更多信息,请查看 OWASP 密码存储备忘单
  • 明确授权资源请求。
  • 利用基于角色的授权,使用 User.Identity.IsInRole

XAML 指南

  • 在应用程序的 Internet 区域安全约束范围内工作。
  • 使用 ClickOnce 部署。为了增强权限,在运行时使用权限提升或在安装时使用受信任的应用程序部署。

Windows 窗体指南

  • 尽可能使用部分信任。部分信任的 Windows 应用程序减少了应用程序的攻击面。管理应用程序必须使用和可能使用的权限列表,然后在运行时以声明方式请求这些权限。
  • 使用 ClickOnce 部署。为了增强权限,在运行时使用权限提升或在安装时使用受信任的应用程序部署。

WCF 指南

  • 请记住,在 RESTful 服务中传递请求的唯一安全方式是通过 HTTP POST,并启用 TLS。使用 HTTP GET 需要将数据放在 URL 中(例如查询字符串),这对于用户是可见的,并将记录并存储在他们的浏览器历史记录中。
  • 避免使用 BasicHttpBinding。它没有默认安全配置。请改用 WSHttpBinding
  • 为您的绑定至少使用两种安全模式。消息安全性包括头部中的安全规定。传输安全性意味着使用 SSL。TransportWithMessageCredential 结合了这两种模式。
  • 使用模糊测试工具(例如 ZAP)测试您的 WCF 实现。