dotnet-security-owasp dotnet-security-owasp

本技能提供基于OWASP Top 10的.NET应用安全加固指南,涵盖注入攻击、访问控制、跨站脚本等漏洞防护,以及已弃用安全API的警告。适用于后端开发人员和安全专家,关键词包括.NET安全、OWASP、应用防护、代码安全、网络安全、软件开发。

后端开发 0 次安装 0 次浏览 更新于 3/6/2026

name: dotnet-security-owasp description: “根据OWASP Top 10加固.NET应用:注入、认证、XSS、已弃用的安全API。” user-invocable: false

dotnet-security-owasp

.NET应用的OWASP Top 10(2021)安全指南。每个类别包括漏洞描述、.NET特定风险、缓解代码示例和常见陷阱。此技能是已弃用安全模式警告(CAS、APTCA、.NET Remoting、DCOM、BinaryFormatter)的规范所有者。

范围

  • 基于OWASP Top 10(2021)漏洞类别的.NET特定缓解措施
  • 注入、访问控制破坏、XSS、SSRF预防模式
  • 已弃用安全API警告(CAS、APTCA、BinaryFormatter、.NET Remoting)
  • 安全头配置和CORS加固
  • 速率限制和防伪中间件模式
  • NuGet包审计和依赖漏洞扫描

超出范围

  • 认证/授权实现 — 参见[skill:dotnet-api-security]
  • Blazor认证UI — 参见[skill:dotnet-blazor-auth]
  • 加密算法选择 — 参见[skill:dotnet-cryptography]
  • 配置绑定和Options模式 — 参见[skill:dotnet-csharp-configuration]
  • 密钥存储和管理 — 参见[skill:dotnet-secrets-management]

交叉引用:[skill:dotnet-secrets-management]用于密钥处理,[skill:dotnet-cryptography]用于加密最佳实践,[skill:dotnet-csharp-coding-standards]用于安全编码惯例。


A01:访问控制破坏

漏洞: 用户在预期权限之外操作 — 访问其他用户数据、提升权限或绕过访问检查。

.NET风险: 控制器/端点缺少[Authorize]属性、不安全的直接对象引用(IDOR)用户ID从路由参数获取而未验证所有权,以及CORS配置错误允许意外来源。

缓解措施

// 1. 全局应用授权,然后显式选择退出
builder.Services.AddAuthorizationBuilder()
    .SetFallbackPolicy(new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build());

var app = builder.Build();
app.MapControllers(); // 所有端点默认需要认证

// 2. 基于资源的授权以防止IDOR
public sealed class DocumentAuthorizationHandler
    : AuthorizationHandler<EditRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        EditRequirement requirement,
        Document resource)
    {
        if (resource.OwnerId == context.User.FindFirstValue(ClaimTypes.NameIdentifier))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}

// 在端点中:
app.MapPut("/documents/{id}", async (
    int id,
    DocumentDto dto,
    IAuthorizationService authService,
    ClaimsPrincipal user,
    AppDbContext db) =>
{
    var document = await db.Documents.FindAsync(id);
    if (document is null) return Results.NotFound();

    var authResult = await authService.AuthorizeAsync(user, document, "Edit");
    if (!authResult.Succeeded) return Results.Forbid();

    document.Title = dto.Title;
    await db.SaveChangesAsync();
    return Results.NoContent();
});
// 3. 将CORS限制到已知来源
builder.Services.AddCors(options =>
{
    options.AddPolicy("Strict", policy =>
    {
        policy.WithOrigins("https://app.example.com")
              .WithMethods("GET", "POST")
              .WithHeaders("Content-Type", "Authorization");
    });
});

陷阱: AllowAnyOrigin()AllowCredentials()组合在ASP.NET Core运行时会被拒绝,但SetIsOriginAllowed(_ => true)AllowCredentials()会静默允许所有来源 — 永远不要使用此模式。


A02:加密失败

漏洞: 敏感数据因弱或缺少加密而暴露 — 明文存储、已弃用算法或不当密钥管理。

.NET风险: 使用MD5/SHA1哈希密码、在appsettings.json中存储带明文密码的连接字符串、通过HTTP传输敏感数据或使用DES/RC2进行加密。

缓解措施

// 强制执行HTTPS和HSTS
builder.Services.AddHttpsRedirection(options =>
{
    options.HttpsPort = 443;
});

var app = builder.Build();
app.UseHsts(); // Strict-Transport-Security头
app.UseHttpsRedirection();

// 永远不要在appsettings.json中存储密钥 — 使用用户密钥或环境变量
// 参见[skill:dotnet-secrets-management]以进行适当的密钥处理
// 使用数据保护API对应用数据进行对称加密
public sealed class TokenProtector(IDataProtectionProvider provider)
{
    private readonly IDataProtector _protector =
        provider.CreateProtector("Tokens.V1");

    public string Protect(string plaintext) => _protector.Protect(plaintext);

    public string Unprotect(string ciphertext) => _protector.Unprotect(ciphertext);
}

参见[skill:dotnet-cryptography]以进行算法选择(AES-GCM、RSA、ECDSA)和密钥派生。


A03:注入

漏洞: 不受信任的数据作为命令或查询的一部分发送到解释器 — SQL注入、命令注入、LDAP注入和跨站脚本(XSS)。

.NET风险: SQL查询中的字符串拼接、带有未消毒输入的Process.Start、在Razor页面中将用户输入渲染为原始HTML。

缓解措施

// SQL注入预防:始终使用参数化查询
// EF Core通过LINQ默认参数化
var orders = await db.Orders
    .Where(o => o.CustomerId == customerId)
    .ToListAsync();

// 当需要原始SQL时,使用参数化插值
var results = await db.Orders
    .FromSqlInterpolated($"SELECT * FROM Orders WHERE Status = {status}")
    .ToListAsync();

// 永远不要将用户输入拼接到SQL中:
// var bad = db.Orders.FromSqlRaw("SELECT * FROM Orders WHERE Status = '" + status + "'");
// XSS预防:Razor默认编码输出。
// 仅对受信任的预消毒HTML使用@Html.Raw()。
// 在Minimal API中,返回类型化结果 — 而非原始字符串:
app.MapGet("/greeting", (string name) =>
    Results.Content($"<p>Hello, {HtmlEncoder.Default.Encode(name)}</p>",
        "text/html"));

// 命令注入预防:避免使用用户输入的Process.Start。
// 如果不可避免,基于白名单验证:
public static bool IsAllowedTool(string toolName) =>
    toolName is "dotnet" or "git" or "nuget";

陷阱: 带有字符串拼接的FromSqlRaw绕过参数化。始终使用FromSqlInterpolated或传递SqlParameter对象到FromSqlRaw


A04:不安全设计

漏洞: 设计模式中的缺陷无法仅通过实现修复 — 缺少速率限制、缺乏深度防御、无限制资源消耗。

.NET风险: 没有速率限制的API、无界文件上传、状态更改操作上缺少防伪令牌。

缓解措施

// 使用内置中间件进行速率限制(.NET 7+)
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("api", limiterOptions =>
    {
        limiterOptions.PermitLimit = 100;
        limiterOptions.Window = TimeSpan.FromMinutes(1);
        limiterOptions.QueueLimit = 0;
    });
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});

var app = builder.Build();
app.UseRateLimiter();

app.MapGet("/api/data", () => Results.Ok("data"))
    .RequireRateLimiting("api");
// Minimal API的防伪(.NET 8+)
builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

// 表单绑定端点:防伪自动验证
app.MapPost("/orders", async ([FromForm] string productId, AppDbContext db) =>
{
    var order = new Order { ProductId = productId };
    db.Orders.Add(order);
    await db.SaveChangesAsync();
    return Results.Created($"/orders/{order.Id}", order);
});

// JSON端点:使用RequireAntiforgery()显式选择加入
app.MapPost("/api/orders", async (CreateOrderDto dto, AppDbContext db) =>
{
    var order = new Order { ProductId = dto.ProductId };
    db.Orders.Add(order);
    await db.SaveChangesAsync();
    return Results.Created($"/api/orders/{order.Id}", order);
}).RequireAntiforgery();

陷阱: UseRateLimiter()必须在UseRouting()之后和MapControllers()/MapGet()之前调用以正确应用。


A05:安全配置错误

漏洞: 不安全的默认配置、不完整配置、开放的云存储、不必要的功能启用、详细错误消息。

.NET风险: 生产环境中的详细异常页面(UseDeveloperExceptionPage)、暴露服务器头的默认Kestrel设置、调试端点启用或缺少安全头。

缓解措施

// 移除服务器身份头(在Build之前配置)
builder.WebHost.ConfigureKestrel(options =>
{
    options.AddServerHeader = false;
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    // 生产环境中的通用错误处理 — 无堆栈跟踪
    app.UseExceptionHandler("/error");
    app.UseHsts();
}

// 通过中间件添加安全头
app.Use(async (context, next) =>
{
    context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
    context.Response.Headers.Append("X-Frame-Options", "DENY");
    context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
    context.Response.Headers.Append(
        "Content-Security-Policy",
        "default-src 'self'; script-src 'self'; style-src 'self'");
    await next();
});
// 限制请求体大小以防止资源耗尽(在Build之前配置)
builder.WebHost.ConfigureKestrel(options =>
{
    options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB
    options.Limits.MaxRequestHeadersTotalSize = 32 * 1024; // 32 KB
    options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
});

// 然后:var app = builder.Build();

陷阱: UseDeveloperExceptionPage()泄漏源代码路径和堆栈跟踪。确保它受IsDevelopment()保护,且永远不要在production或staging中启用。


A06:易受攻击和过时组件

漏洞: 使用具有已知漏洞的组件、不受支持的框架或未修补的依赖。

.NET风险: 运行在过时支持的.NET版本、具有已知CVE的NuGet包、未审计的传递依赖漏洞。

缓解措施

<!-- 在Directory.Build.props或csproj中启用NuGet审计 -->
<PropertyGroup>
  <NuGetAudit>true</NuGetAudit>
  <NuGetAuditLevel>low</NuGetAuditLevel>
  <NuGetAuditMode>all</NuGetAuditMode> <!-- 审计直接 + 传递 -->
</PropertyGroup>
# 审计NuGet包以获取已知漏洞
dotnet list package --vulnerable --include-transitive

# 保持包最新
dotnet outdated  # 需要dotnet-outdated-tool

# 检查.NET SDK/运行时支持状态
dotnet --info

陷阱: NuGetAuditMode默认为direct — 传递漏洞被隐藏,除非设置all。在CI中始终使用all以捕获深层依赖问题。


A07:身份认证和认证失败

漏洞: 弱认证机制、凭证填充、会话固定、缺少多因素认证。

.NET风险: 默认Identity密码策略太弱、没有Secure/SameSite属性的会话Cookie、缺少账户锁定配置。

缓解措施

// 配置强Identity密码和锁定策略
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
    // 密码要求
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 12;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireLowercase = true;

    // 账户锁定
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.AllowedForNewUsers = true;

    // 用户设置
    options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
// 安全Cookie配置
builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Strict;
    options.ExpireTimeSpan = TimeSpan.FromHours(2);
    options.SlidingExpiration = true;
});

陷阱: CookieSecurePolicy.SameAsRequest允许在开发环境中通过HTTP的Cookie,这没问题。但在反向代理终止TLS的生产环境中,应用看到HTTP — 因此Cookie以不安全方式发送。在生产中始终使用CookieSecurePolicy.Always并配置转发头。


A08:软件和数据完整性失败

漏洞: 未防止完整性违规的代码和基础设施 — 未签名包、不安全的CI/CD管道、反序列化不受信任数据。

.NET风险: 使用BinaryFormatter进行反序列化(任意代码执行)、从未受信任源接受未签名NuGet包、缺少包源映射。

缓解措施

<!-- nuget.config中的NuGet包源映射 -->
<!-- 仅允许来自受信任源的包 -->
<configuration>
  <packageSources>
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
    <add key="internal" value="https://pkgs.example.com/nuget/v3/index.json" />
  </packageSources>
  <packageSourceMapping>
    <packageSource key="nuget.org">
      <package pattern="*" />
    </packageSource>
    <packageSource key="internal">
      <package pattern="MyCompany.*" />
    </packageSource>
  </packageSourceMapping>
</configuration>
// 永远不要使用BinaryFormatter — 它是一个关键的反序列化漏洞。
// BinaryFormatter在.NET 8+中已过时为错误(SYSLIB0011),在.NET 9+中移除。
// 改为使用System.Text.Json:
var data = JsonSerializer.Deserialize<OrderDto>(jsonString);

// 对于二进制序列化需求,使用MessagePack或Protobuf:
// <PackageReference Include="MessagePack" Version="3.*" />
var bytes = MessagePackSerializer.Serialize(order);
var restored = MessagePackSerializer.Deserialize<Order>(bytes);

陷阱: 包源映射使用最特定模式优先:MyCompany.*击败*通配符。始终为内部包定义特定模式以防止依赖混淆攻击。


A09:安全日志和监控失败

漏洞: 安全相关事件的日志记录不足、缺乏对入侵的监控、无法检测和响应主动攻击。

.NET风险: 未记录认证失败、敏感操作缺少审计跟踪、以明文记录敏感数据(密码、令牌)。

缓解措施

// 使用结构化日志记录安全事件
public sealed class AuditMiddleware(RequestDelegate next, ILogger<AuditMiddleware> logger)
{
    public async Task InvokeAsync(HttpContext context)
    {
        var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous";
        var path = context.Request.Path.Value;

        using (logger.BeginScope(new Dictionary<string, object?>
        {
            ["UserId"] = userId,
            ["RequestPath"] = path,
            ["RemoteIp"] = context.Connection.RemoteIpAddress?.ToString()
        }))
        {
            await next(context);

            // 记录失败的认证尝试
            if (context.Response.StatusCode == StatusCodes.Status401Unauthorized)
            {
                logger.LogWarning("认证失败于 {Path}", path);
            }

            // 记录授权失败
            if (context.Response.StatusCode == StatusCodes.Status403Forbidden)
            {
                logger.LogWarning("授权拒绝于 {Path}", path);
            }
        }
    }
}
// 永远不要记录敏感数据 — 掩盖凭证和PII
// 配置日志过滤以排除敏感路径
builder.Logging.AddFilter("Microsoft.AspNetCore.Authentication", LogLevel.Warning);

// 使用IHttpLoggingInterceptor(.NET 8+)以掩盖请求/响应头
builder.Services.AddHttpLogging(options =>
{
    options.LoggingFields = HttpLoggingFields.RequestPath
        | HttpLoggingFields.RequestMethod
        | HttpLoggingFields.ResponseStatusCode
        | HttpLoggingFields.Duration;
    // 显式排除请求/响应体和认证头
});

陷阱: 使用{Placeholder}语法的结构化日志是安全的,但日志调用中的字符串插值($"用户 {userId}")绕过结构化日志,并可能将PII泄漏到不支持掩盖的日志接收器中。


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

漏洞: 应用基于用户提供的URL获取远程资源而未验证,允许攻击者访问内部服务或元数据端点。

.NET风险: 带有用户提供URL的HttpClient调用、URL重定向跟随到内部网络、访问云元数据端点(169.254.169.254)。

缓解措施

// 验证和限制出站URL
public static class UrlValidator
{
    private static readonly HashSet<string> AllowedHosts = new(StringComparer.OrdinalIgnoreCase)
    {
        "api.example.com",
        "cdn.example.com"
    };

    public static bool IsAllowed(string url)
    {
        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
            return false;

        // 阻止非HTTPS
        if (uri.Scheme != Uri.UriSchemeHttps)
            return false;

        // 阻止私有/内部IP
        if (IPAddress.TryParse(uri.Host, out var ip))
        {
            if (IsPrivateOrReserved(ip))
                return false;
        }

        // 白名单主机
        return AllowedHosts.Contains(uri.Host);
    }

    private static bool IsPrivateOrReserved(IPAddress ip)
    {
        byte[] bytes = ip.GetAddressBytes();
        return bytes[0] switch
        {
            10 => true,                                         // 10.0.0.0/8
            127 => true,                                        // 127.0.0.0/8
            169 when bytes[1] == 254 => true,                   // 169.254.0.0/16(本地链路 / 云元数据)
            172 when bytes[1] >= 16 && bytes[1] <= 31 => true,  // 172.16.0.0/12
            192 when bytes[1] == 168 => true,                   // 192.168.0.0/16
            _ => false
        };
    }
}

// 在端点中使用
app.MapPost("/fetch", async (FetchRequest request, IHttpClientFactory factory) =>
{
    if (!UrlValidator.IsAllowed(request.Url))
        return Results.BadRequest("URL不允许");

    var client = factory.CreateClient();
    var response = await client.GetStringAsync(request.Url);
    return Results.Ok(response);
});
// 配置HttpClient以禁用自动重定向跟随
builder.Services.AddHttpClient("external", client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
    AllowAutoRedirect = false // 防止基于重定向的SSRF
});

陷阱: DNS重新绑定可以绕过IP白名单 — 攻击者的域在验证期间解析为公共IP,但在实际请求期间解析为内部IP。固定DNS解析或在连接后重新验证。


已弃用安全模式

此技能是已弃用安全模式警告的规范所有者。其他技能应交叉引用此处,而非复制这些警告。

代码访问安全(CAS)

CAS在.NET Core/.NET 5+中不支持。引用System.Security.PermissionsSecurityPermission或用于CAS目的的[SecurityCritical]/[SecuritySafeCritical]属性的代码必须移除或替换为操作系统级安全边界(容器、进程隔离)。

AllowPartiallyTrustedCallers(APTCA)

[AllowPartiallyTrustedCallers]属性在.NET Core/.NET 5+中无效。部分信任模型已消失。在迁移过程中移除APTCA属性。改为使用标准授权和输入验证。

.NET Remoting

.NET Remoting在.NET Core/.NET 5+中不可用。它由于远程对象的无限制反序列化而天生不安全。替换为:

  • gRPC用于跨进程/跨机器RPC(参见[skill:dotnet-cryptography]以获取传输安全)
  • 命名管道用于同机器IPC
  • HTTP API用于服务到服务通信

DCOM

分布式COM(DCOM)是仅限Windows且不支持于.NET Core/.NET 5+。替换为gRPC、REST API或消息队列以进行分布式通信。

BinaryFormatter

BinaryFormatter在.NET 8中已过时为错误(SYSLIB0011),并在.NET 9+中移除。它通过反序列化攻击启用任意代码执行。替换为:

  • System.Text.Json用于JSON序列化
  • MessagePack或Protocol Buffers用于二进制格式
  • XmlSerializer带有严格类型白名单用于XML场景

不要将System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization设置为true作为变通方案。


代理陷阱

  1. 不要使用[AllowAnonymous]而无显式理由 — 它覆盖全局回退策略。用注释标记每个匿名端点,解释原因。
  2. 不要为了方便而禁用HTTPS重定向 — 改为在本地开发中使用dotnet dev-certs https --trust
  3. 不要记录原始请求体 — 它们可能包含凭证、令牌或PII。使用HttpLoggingFields以选择安全字段。
  4. 不要仅依赖客户端验证 — 始终在服务器端验证。Razor表单验证是为了用户体验,而非安全。
  5. 不要将字符串插值与FromSqlRaw一起使用 — 使用自动参数化的FromSqlInterpolated
  6. 不要在appsettings.json中存储密钥 — 在开发中使用用户密钥,在生产中使用环境变量或托管身份。参见[skill:dotnet-secrets-management]。
  7. 不要使用已弃用模式生成安全敏感代码 — CAS、APTCA、.NET Remoting、DCOM和BinaryFormatter在现代.NET中均不支持。参见上文的已弃用安全模式部分。

先决条件

  • .NET 8.0+(LTS基线)
  • ASP.NET Core 8.0+用于安全中间件、防伪和速率限制
  • Microsoft.AspNetCore.Identity用于认证/身份(如果使用A07模式)

参考文献