关于 dotnet-security-owasp
dotnet-security-owasp 面向 .NET 开发者,按 OWASP Top 10(2021)组织安全指导,并结合 .NET 特定风险、缓解示例和常见陷阱。
主要功能
- 01覆盖 OWASP Top 10(2021)
- 02说明 .NET 特定漏洞风险
- 03提供缓解措施代码示例
- 04提示常见安全陷阱
- 05警示已弃用的安全模式
适用场景
- 01审查 .NET 代码中的 OWASP 相关风险
- 02为常见应用漏洞选择缓解措施
- 03识别不安全或已弃用的 .NET 安全模式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-security-owasp安装后在 Claude Code 中输入/dotnet-security-owasp确认已加载。
详细介绍
dotnet-security-owasp
适用于 .NET 应用程序的 OWASP Top 10(2021)安全指南。每个类别都包含漏洞说明、.NET 特定风险、缓解措施代码示例和常见陷阱。此技能是已弃用安全模式警告(CAS、APTCA、.NET Remoting、DCOM、BinaryFormatter)的权威来源。
**不在范围内:**身份验证/授权实现——API 级身份验证模式(Identity、OAuth/OIDC、JWT、passkeys、CORS)——请参阅 [skill:dotnet-api-security]。Blazor 身份验证 UI(AuthorizeView、CascadingAuthenticationState)——请参阅 [skill:dotnet-blazor-auth]。特定于云的安全服务(Azure Key Vault、AWS Secrets Manager)——请参阅云相关专题。加密算法选择和密钥管理——请参阅 [skill:dotnet-cryptography]。配置绑定和 Options 模式——请参阅 [skill:dotnet-csharp-configuration]。
交叉引用:[skill:dotnet-secrets-management] 了解机密处理,[skill:dotnet-cryptography] 了解加密最佳实践,[skill:dotnet-csharp-coding-standards] 了解安全编码规范。
A01:访问控制失效
**漏洞:**用户执行超出其预期权限的操作——访问其他用户的数据、提升权限或绕过访问检查。
**.NET 风险:**控制器/端点缺少 [Authorize] 属性;存在不安全的直接对象引用(IDOR),即从路由参数获取用户 ID,却不验证所有权;以及 CORS 配置错误,允许非预期的来源。
缓解措施
// 1. Apply authorization globally, then opt out explicitly
builder.Services.AddAuthorizationBuilder()
.SetFallbackPolicy(new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build());
var app = builder.Build();
app.MapControllers(); // All endpoints require auth by default
// 2. Resource-based authorization to prevent 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;
}
}
// In the endpoint:
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. Restrict CORS to known origins
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 加密。
缓解措施
// Enforce HTTPS and HSTS
builder.Services.AddHttpsRedirection(options =>
{
options.HttpsPort = 443;
});
var app = builder.Build();
app.UseHsts(); // Strict-Transport-Security header
app.UseHttpsRedirection();
// Never store secrets in appsettings.json -- use user secrets or env vars
// See [skill:dotnet-secrets-management] for proper secrets handling
// Use Data Protection API for symmetric encryption of application data
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);
}
有关算法选择(AES-GCM、RSA、ECDSA)和密钥派生,请参阅 [skill:dotnet-cryptography]。
A03:注入
**漏洞:**将不可信数据作为命令或查询的一部分发送给解释器——SQL 注入、命令注入、LDAP 注入和跨站脚本(XSS)。
**.NET 风险:**在 SQL 查询中拼接字符串、使用 Process.Start 处理未经清理的输入,以及在 Razor 页面中将用户输入作为原始 HTML 呈现。
缓解措施
// SQL injection prevention: always use parameterized queries
// EF Core is parameterized by default via LINQ
var orders = await db.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync();
// When raw SQL is needed, use parameterized interpolation
var results = await db.Orders
.FromSqlInterpolated($"SELECT * FROM Orders WHERE Status = {status}")
.ToListAsync();
// NEVER concatenate user input into SQL:
// var bad = db.Orders.FromSqlRaw("SELECT * FROM Orders WHERE Status = '" + status + "'");
// XSS prevention: Razor encodes output by default.
// Use @Html.Raw() ONLY for trusted, pre-sanitized HTML.
// In Minimal APIs, return typed results -- not raw strings:
app.MapGet("/greeting", (string name) =>
Results.Content($"<p>Hello, {HtmlEncoder.Default.Encode(name)}</p>",
"text/html"));
// Command injection prevention: avoid Process.Start with user input.
// If unavoidable, validate against an allowlist:
public static bool IsAllowedTool(string toolName) =>
toolName is "dotnet" or "git" or "nuget";
注意:FromSqlRaw 与字符串拼接结合使用会绕过参数化。请始终使用 FromSqlInterpolated,或将 SqlParameter 对象传递给 FromSqlRaw。
A04:不安全设计
**漏洞:**设计模式中的缺陷无法仅通过实现来修复——缺少速率限制、缺乏纵深防御、资源消耗不受限制。
**.NET 风险:**未实施速率限制的 API、不受限制的文件上传,以及状态变更操作缺少防伪令牌。
缓解措施
// Rate limiting with built-in middleware (.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");
// Anti-forgery for Minimal APIs (.NET 8+)
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
// Form-bound endpoint: antiforgery validated automatically
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 endpoint: opt in explicitly with 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();
**注意:**必须在 UseRouting() 之后、MapControllers()/MapGet() 之前调用 UseRateLimiter(),才能正确生效。
A05:安全配置错误
**漏洞:**默认配置不安全、配置不完整、云存储开放、不必要的功能处于启用状态,以及错误消息过于详细。
**.NET 风险:**在生产环境中显示详细异常页面(UseDeveloperExceptionPage)、Kestrel 默认设置暴露服务器标头、调试端点仍处于启用状态,或缺少安全标头。
缓解措施
// Remove server identity headers (configure BEFORE Build)
builder.WebHost.ConfigureKestrel(options =>
{
options.AddServerHeader = false;
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// Generic error handler in production -- no stack traces
app.UseExceptionHandler("/error");
app.UseHsts();
}
// Add security headers via middleware
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();
});
// Constrain request body size to prevent resource exhaustion (configure BEFORE 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);
});
// Then: var app = builder.Build();
注意:UseDeveloperExceptionPage() 会泄露源代码路径和堆栈跟踪。请确保仅在 IsDevelopment() 条件下启用,绝不要在生产或预发布环境中启用。
A06:易受攻击和过时的组件
漏洞: 使用存在已知漏洞的组件、不受支持的框架或未修补的依赖项。
.NET 中的风险: 在已停止支持的 .NET 版本上运行,使用存在已知 CVE 的 NuGet 包,未审计传递依赖项漏洞。
缓解措施
<PropertyGroup>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditLevel>low</NuGetAuditLevel>
<NuGetAuditMode>all</NuGetAuditMode>
</PropertyGroup>
# Audit NuGet packages for known vulnerabilities
dotnet list package --vulnerable --include-transitive
# Keep packages up to date
dotnet outdated # requires dotnet-outdated-tool
# Check .NET SDK/runtime support status
dotnet --info
注意事项: NuGetAuditMode 默认值为 direct——除非设置为 all,否则会隐藏传递依赖项漏洞。在 CI 中始终使用 all,以发现深层依赖项问题。
A07:身份识别和身份验证失败
漏洞: 身份验证机制薄弱、凭据填充攻击、会话固定、缺少多重身份验证。
.NET 中的风险: 默认 Identity 密码策略过于宽松,会话 Cookie 缺少 Secure/SameSite 属性,未配置账户锁定。
缓解措施
// Configure strong Identity password and lockout policies
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
// Password requirements
options.Password.RequireDigit = true;
options.Password.RequiredLength = 12;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
// Account lockout
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
// Secure cookie configuration
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 包,未配置包源映射。
缓解措施
<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>
// NEVER use BinaryFormatter -- it is a critical deserialization vulnerability.
// BinaryFormatter is obsolete as error (SYSLIB0011) in .NET 8+ and removed in .NET 9+.
// Use System.Text.Json instead:
var data = JsonSerializer.Deserialize<OrderDto>(jsonString);
// For binary serialization needs, use MessagePack or Protobuf:
// <PackageReference Include="MessagePack" Version="3.*" />
var bytes = MessagePackSerializer.Serialize(order);
var restored = MessagePackSerializer.Deserialize<Order>(bytes);
注意事项: 包源映射采用最具体模式优先规则:MyCompany.* 优先于 * 通配符。始终为内部包定义具体模式,以防范依赖项混淆攻击。
A09:安全日志记录和监控失败
漏洞: 未能充分记录与安全相关的事件,缺乏对安全漏洞的监控,无法检测和响应正在进行的攻击。
.NET 中的风险: 未记录身份验证失败,缺少敏感操作的审计跟踪,以明文形式记录敏感数据(密码、令牌)。
缓解措施
// Log security events with structured logging
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);
// Log failed authentication attempts
if (context.Response.StatusCode == StatusCodes.Status401Unauthorized)
{
logger.LogWarning("Authentication failed for {Path}", path);
}
// Log authorization failures
if (context.Response.StatusCode == StatusCodes.Status403Forbidden)
{
logger.LogWarning("Authorization denied for {Path}", path);
}
}
}
}
// NEVER log sensitive data -- redact credentials and PII
// Configure log filtering to exclude sensitive paths
builder.Logging.AddFilter("Microsoft.AspNetCore.Authentication", LogLevel.Warning);
// Use IHttpLoggingInterceptor (.NET 8+) to redact request/response headers
builder.Services.AddHttpLogging(options =>
{
options.LoggingFields = HttpLoggingFields.RequestPath
| HttpLoggingFields.RequestMethod
| HttpLoggingFields.ResponseStatusCode
| HttpLoggingFields.Duration;
// Explicitly exclude request/response bodies and auth headers
});
注意事项: 使用 {Placeholder} 语法的结构化日志记录是安全的,但在日志调用中使用字符串插值($"User {userId}")会绕过结构化日志记录,并可能将个人身份信息泄露到不支持数据脱敏的日志接收端。
A10:服务器端请求伪造(SSRF)
漏洞: 应用根据用户提供的 URL 获取远程资源,却未进行验证,因此攻击者可以访问内部服务或元数据端点。
.NET 中的风险: 使用用户提供的 URL 调用 HttpClient,跟随 URL 重定向访问内部网络,访问云元数据端点(169.254.169.254)。
缓解措施
// Validate and restrict outbound URLs
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;
// Block non-HTTPS
if (uri.Scheme != Uri.UriSchemeHttps)
return false;
// Block private/internal IPs
if (IPAddress.TryParse(uri.Host, out var ip))
{
if (IsPrivateOrReserved(ip))
return false;
}
// Allowlist hosts
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 (link-local / cloud metadata)
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
};
}
}
// Usage in an endpoint
app.MapPost("/fetch", async (FetchRequest request, IHttpClientFactory factory) =>
{
if (!UrlValidator.IsAllowed(request.Url))
return Results.BadRequest("URL not allowed");
var client = factory.CreateClient();
var response = await client.GetStringAsync(request.Url);
return Results.Ok(response);
});
// Configure HttpClient to disable automatic redirect following
builder.Services.AddHttpClient("external", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
AllowAutoRedirect = false // Prevent redirect-based SSRF
});
注意事项: DNS 重绑定可以绕过 IP 允许列表——攻击者的域名在验证期间解析为公共 IP,但在实际请求期间解析为内部 IP。固定 DNS 解析结果,或在建立连接后重新验证。
已弃用的安全模式
本技能是已弃用安全模式警告的权威来源。其他技能应交叉引用此处,而不是重复这些警告。
代码访问安全(CAS)
.NET Core/.NET 5+ 不支持 CAS。出于 CAS 目的而引用 System.Security.Permissions、SecurityPermission 或 [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
在 .NET 8 中,BinaryFormatter 已过时并会报错(SYSLIB0011);在 .NET 9+ 中已移除。它会通过反序列化攻击导致任意代码执行。请替换为:
- 使用
System.Text.Json进行 JSON 序列化 - 使用 MessagePack 或 Protocol Buffers 处理二进制格式
- 在 XML 场景中使用
XmlSerializer,并严格限制允许的类型
不要将 System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization 设为 true 来绕过此问题。
Agent 注意事项
- 不要在没有明确理由的情况下使用
[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 模式)