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.Permissions、SecurityPermission或用于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作为变通方案。
代理陷阱
- 不要使用
[AllowAnonymous]而无显式理由 — 它覆盖全局回退策略。用注释标记每个匿名端点,解释原因。 - 不要为了方便而禁用HTTPS重定向 — 改为在本地开发中使用
dotnet dev-certs https --trust。 - 不要记录原始请求体 — 它们可能包含凭证、令牌或PII。使用
HttpLoggingFields以选择安全字段。 - 不要仅依赖客户端验证 — 始终在服务器端验证。Razor表单验证是为了用户体验,而非安全。
- 不要将字符串插值与
FromSqlRaw一起使用 — 使用自动参数化的FromSqlInterpolated。 - 不要在
appsettings.json中存储密钥 — 在开发中使用用户密钥,在生产中使用环境变量或托管身份。参见[skill:dotnet-secrets-management]。 - 不要使用已弃用模式生成安全敏感代码 — CAS、APTCA、.NET Remoting、DCOM和BinaryFormatter在现代.NET中均不支持。参见上文的已弃用安全模式部分。
先决条件
- .NET 8.0+(LTS基线)
- ASP.NET Core 8.0+用于安全中间件、防伪和速率限制
- Microsoft.AspNetCore.Identity用于认证/身份(如果使用A07模式)