返回 Skill 列表
extension
分类: 开发与工程无需 API Key

dotnet-security

.NET和ASP.NET Core安全模式。涵盖身份验证、认证、依赖审核、安全编码实践以及.NET生态系统中的OWASP。使用场景:用户使用“C#”、“.NET”、“ASP.NET Core”、“Entity Framework”,询问关于“.NET漏洞”、“NuGet安全性”、“.NET认证”、“Blazor安全性”。不适用于:通用的OWASP概念 - 应使用`owasp`或`owasp-top-10`,Java/Python安全性 - 应使用特定语言的安全技能。

person作者: jakexiaohubgithub

.NET Security - Quick Reference

When NOT to Use This Skill

  • General OWASP concepts - Use owasp or owasp-top-10 skill
  • Java security - Use java-security skill
  • Python security - Use python-security skill
  • Secrets management - Use secrets-management skill

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: dotnet for ASP.NET Core security documentation.

Dependency Auditing

# .NET built-in audit
dotnet list package --vulnerable

# Detailed audit with transitive dependencies
dotnet list package --vulnerable --include-transitive

# Check outdated packages
dotnet list package --outdated

# Snyk for .NET
snyk test

CI/CD Integration

# GitHub Actions
- name: Security audit
  run: |
    dotnet list package --vulnerable --include-transitive
    dotnet tool install -g snyk
    snyk test

NuGet.config Security

<configuration>
  <packageSources>
    <!-- Use only trusted sources -->
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
  <packageSourceCredentials>
    <!-- Use environment variables for private feeds -->
  </packageSourceCredentials>
</configuration>

ASP.NET Core Security Configuration

Program.cs Security Setup

var builder = WebApplication.CreateBuilder(args);

// Security headers
builder.Services.AddHsts(options =>
{
    options.MaxAge = TimeSpan.FromDays(365);
    options.IncludeSubDomains = true;
    options.Preload = true;
});

// CORS configuration
builder.Services.AddCors(options =>
{
    options.AddPolicy("Production", policy =>
    {
        policy.WithOrigins("https://myapp.com")
              .AllowCredentials()
              .WithMethods("GET", "POST", "PUT", "DELETE")
              .WithHeaders("Authorization", "Content-Type");
    });
});

// Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });

// Rate limiting (.NET 7+)
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("login", limiter =>
    {
        limiter.Window = TimeSpan.FromMinutes(15);
        limiter.PermitLimit = 5;
        limiter.QueueLimit = 0;
    });
});

var app = builder.Build();

// Security middleware order matters
app.UseHsts();
app.UseHttpsRedirection();
app.UseCors("Production");
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();

Security Headers Middleware

app.Use(async (context, next) =>
{
    context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
    context.Response.Headers.Add("X-Frame-Options", "DENY");
    context.Response.Headers.Add("X-XSS-Protection", "0"); // Use CSP instead
    context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");
    context.Response.Headers.Add("Content-Security-Policy",
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");

    await next();
});

SQL Injection Prevention

Entity Framework Core - Safe

// SAFE - LINQ queries
var user = await context.Users
    .FirstOrDefaultAsync(u => u.Email == email);

// SAFE - Parameterized raw SQL
var users = await context.Users
    .FromSqlRaw("SELECT * FROM Users WHERE Email = {0}", email)
    .ToListAsync();

// SAFE - Interpolated (converted to parameters)
var users = await context.Users
    .FromSqlInterpolated($"SELECT * FROM Users WHERE Email = {email}")
    .ToListAsync();

Entity Framework Core - UNSAFE

// UNSAFE - String concatenation
var query = $"SELECT * FROM Users WHERE Email = '{email}'";  // NEVER!
var users = await context.Users.FromSqlRaw(query).ToListAsync();

// UNSAFE - FormattableString with raw
var users = await context.Users
    .FromSqlRaw($"SELECT * FROM Users WHERE Email = '{email}'")  // NEVER!
    .ToListAsync();

Dapper - Safe

// SAFE - Anonymous parameters
var user = await connection.QueryFirstOrDefaultAsync<User>(
    "SELECT * FROM Users WHERE Email = @Email",
    new { Email = email }
);

// SAFE - DynamicParameters
var parameters = new DynamicParameters();
parameters.Add("Email", email);
var user = await connection.QueryFirstOrDefaultAsync<User>(
    "SELECT * FROM Users WHERE Email = @Email",
    parameters
);

XSS Prevention

Razor Pages (Auto-encoding)

<!-- SAFE - Auto-encoded -->
<p>@Model.UserInput</p>

<!-- UNSAFE - Raw HTML -->
<p>@Html.Raw(Model.UserInput)</p>  <!-- Avoid if possible -->

Manual Sanitization

using Ganss.Xss;

var sanitizer = new HtmlSanitizer();
sanitizer.AllowedTags.Add("p");
sanitizer.AllowedTags.Add("b");
sanitizer.AllowedTags.Add("i");

string safeHtml = sanitizer.Sanitize(userInput);

API Response Encoding

using System.Text.Encodings.Web;

var encoder = HtmlEncoder.Default;
var safeString = encoder.Encode(userInput);

Authentication & Authorization

JWT Token Generation

public class JwtService
{
    private readonly IConfiguration _config;

    public JwtService(IConfiguration config) => _config = config;

    public string GenerateToken(User user)
    {
        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Email, user.Email),
            new Claim(ClaimTypes.Role, user.Role)
        };

        var token = new JwtSecurityToken(
            issuer: _config["Jwt:Issuer"],
            audience: _config["Jwt:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddHours(1),
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

Password Hashing with Identity

// Use ASP.NET Core Identity's PasswordHasher
var hasher = new PasswordHasher<User>();

// Hash password
string hashed = hasher.HashPassword(user, password);

// Verify password
var result = hasher.VerifyHashedPassword(user, hashed, password);
if (result == PasswordVerificationResult.Success)
{
    // Password matches
}

Authorization Policies

// Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin"));

    options.AddPolicy("ResourceOwner", policy =>
        policy.Requirements.Add(new ResourceOwnerRequirement()));
});

// Custom requirement handler
public class ResourceOwnerHandler : AuthorizationHandler<ResourceOwnerRequirement, Resource>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        ResourceOwnerRequirement requirement,
        Resource resource)
    {
        var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (resource.OwnerId == userId)
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}

// Controller usage
[Authorize(Policy = "AdminOnly")]
public IActionResult AdminDashboard() => View();

Input Validation

Data Annotations

public class CreateUserRequest
{
    [Required]
    [EmailAddress]
    [StringLength(255)]
    public string Email { get; set; } = default!;

    [Required]
    [StringLength(128, MinimumLength = 12)]
    [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).+$",
        ErrorMessage = "Password must contain uppercase, lowercase, number and special character")]
    public string Password { get; set; } = default!;

    [Required]
    [StringLength(100, MinimumLength = 2)]
    [RegularExpression(@"^[a-zA-Z\s\-']+$")]
    public string Name { get; set; } = default!;
}

[HttpPost]
public IActionResult CreateUser([FromBody] CreateUserRequest request)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    // request is validated
}

FluentValidation

public class CreateUserValidator : AbstractValidator<CreateUserRequest>
{
    public CreateUserValidator()
    {
        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .MaximumLength(255);

        RuleFor(x => x.Password)
            .NotEmpty()
            .MinimumLength(12)
            .MaximumLength(128)
            .Matches(@"[A-Z]").WithMessage("Must contain uppercase")
            .Matches(@"[a-z]").WithMessage("Must contain lowercase")
            .Matches(@"\d").WithMessage("Must contain digit")
            .Matches(@"[@$!%*?&]").WithMessage("Must contain special character");

        RuleFor(x => x.Name)
            .NotEmpty()
            .Length(2, 100)
            .Matches(@"^[a-zA-Z\s\-']+$");
    }
}

Secure File Upload

[HttpPost("upload")]
[RequestSizeLimit(10 * 1024 * 1024)] // 10 MB
public async Task<IActionResult> Upload(IFormFile file)
{
    // Validate content type
    var allowedTypes = new[] { "image/jpeg", "image/png", "application/pdf" };
    if (!allowedTypes.Contains(file.ContentType))
        return BadRequest("File type not allowed");

    // Validate file extension
    var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".pdf" };
    var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
    if (!allowedExtensions.Contains(extension))
        return BadRequest("File extension not allowed");

    // Generate safe filename
    var safeName = $"{Guid.NewGuid()}{extension}";
    var uploadPath = Path.Combine(_uploadDirectory, safeName);

    // Save file
    await using var stream = new FileStream(uploadPath, FileMode.Create);
    await file.CopyToAsync(stream);

    return Ok(new { filename = safeName });
}

Secrets Management

User Secrets (Development)

# Initialize user secrets
dotnet user-secrets init

# Set secrets
dotnet user-secrets set "Jwt:Key" "your-secret-key"
dotnet user-secrets set "ConnectionStrings:Default" "your-connection-string"

appsettings.json (DO NOT store secrets)

{
  "Jwt": {
    "Issuer": "https://myapp.com",
    "Audience": "https://myapp.com"
    // Key should come from environment or secrets manager
  }
}

Environment Variables

// Program.cs - Load from environment
builder.Configuration.AddEnvironmentVariables();

// Access
var jwtKey = builder.Configuration["Jwt:Key"]
    ?? throw new InvalidOperationException("JWT Key not configured");

Azure Key Vault Integration

builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{vaultName}.vault.azure.net/"),
    new DefaultAzureCredential()
);

Logging Security Events

public class SecurityLogger
{
    private readonly ILogger<SecurityLogger> _logger;

    public SecurityLogger(ILogger<SecurityLogger> logger) => _logger = logger;

    public void LogLoginAttempt(string username, bool success, string ipAddress)
    {
        _logger.LogInformation(
            "Login attempt: User={Username}, Success={Success}, IP={IpAddress}",
            username, success, ipAddress
        );
    }

    public void LogAccessDenied(string userId, string resource, string ipAddress)
    {
        _logger.LogWarning(
            "Access denied: User={UserId}, Resource={Resource}, IP={IpAddress}",
            userId, resource, ipAddress
        );
    }

    // NEVER log sensitive data
    // _logger.LogInformation("Password: {Password}", password);  // NEVER!
}

Anti-Patterns

| Anti-Pattern | Why It's Bad | Correct Approach | |--------------|--------------|------------------| | String interpolation in SQL | SQL injection | Use parameterized queries | | Html.Raw(userInput) | XSS vulnerability | Use default encoding | | Storing secrets in appsettings | Secret exposure | Use User Secrets/Key Vault | | [AllowAnonymous] everywhere | No authentication | Apply selectively | | Disabling HTTPS redirection | Man-in-the-middle | Keep HTTPS enabled | | Custom crypto implementation | Weak encryption | Use built-in libraries | | Catching all exceptions | Hides security issues | Log and handle specifically |

Quick Troubleshooting

| Issue | Likely Cause | Solution | |-------|--------------|----------| | 401 Unauthorized | JWT validation failed | Check issuer, audience, key | | CORS error | Origin not allowed | Add origin to CORS policy | | Rate limit triggered | Too many requests | Adjust rate limiter settings | | Password validation fails | Policy requirements | Check Identity password options | | NuGet vulnerability | Outdated package | Update to patched version | | User Secrets not loading | Not in Development | Check ASPNETCORE_ENVIRONMENT |

Security Scanning Commands

# Dependency audit
dotnet list package --vulnerable --include-transitive

# Security analyzers (add NuGet packages)
# Microsoft.CodeAnalysis.NetAnalyzers
# SecurityCodeScan.VS2019

# Snyk
snyk test

# Check for secrets
gitleaks detect
trufflehog git file://.

# SAST with Semgrep
semgrep --config=p/csharp .

Related Skills