dotnet-security について
.NET 開発者向けのセキュリティリファレンススキルです。依存関係の監査、ASP.NET Core の設定、入力処理、認証に加え、コード例やアンチパターン、トラブルシューティングも扱います。
主な機能
- 01脆弱・古い NuGet パッケージの監査
- 02ASP.NET Core のセキュリティヘッダー設定
- 03SQL インジェクションと XSS 対策の確認
- 04認証・認可・入力検証の解説
- 05ファイルアップロードとシークレット管理の助言
- 06アンチパターンと問題の切り分け
活用シーン
- 01リリース前の .NET 依存関係の脆弱性確認
- 02ASP.NET Core アプリのセキュリティ設定
- 03データベースクエリとユーザー入力処理のレビュー
- 04JWT、CORS、NuGet のセキュリティ問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill dotnet-securityインストール後、Claude Code で/dotnet-securityを入力して読み込まれたか確認します。
詳細
.NET セキュリティ - クイックリファレンス
このスキルを使用しない場合
- 一般的な OWASP の概念 -
owaspまたはowasp-top-10スキルを使用してください - Java のセキュリティ -
java-securityスキルを使用してください - Python のセキュリティ -
python-securityスキルを使用してください - シークレット管理 -
secrets-managementスキルを使用してください
詳細な知識:ASP.NET Core のセキュリティに関するドキュメントには、technology:
dotnetを指定してmcp__documentation__fetch_docsを使用してください。
依存関係の監査
# .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 との統合
# GitHub Actions
- name: Security audit
run: |
dotnet list package --vulnerable --include-transitive
dotnet tool install -g snyk
snyk test
NuGet.config のセキュリティ
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceCredentials>
</packageSourceCredentials>
</configuration>
ASP.NET Core のセキュリティ設定
Program.cs のセキュリティ設定
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();
セキュリティヘッダーのミドルウェア
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 インジェクションの防止
Entity Framework Core - 安全
// 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 - 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 - 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 の防止
Razor Pages(自動エンコード)
<p>@Model.UserInput</p>
<p>@Html.Raw(Model.UserInput)</p>
手動でのサニタイズ
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 レスポンスのエンコード
using System.Text.Encodings.Web;
var encoder = HtmlEncoder.Default;
var safeString = encoder.Encode(userInput);
認証と認可
JWT トークンの生成
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);
}
}
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
}
認可ポリシー
// 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();
入力の検証
データアノテーション
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\-']+$");
}
}
安全なファイルアップロード
[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 });
}
シークレット管理
User Secrets(開発環境)
# 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(シークレットを保存しないでください)
{
"Jwt": {
"Issuer": "https://myapp.com",
"Audience": "https://myapp.com"
// Key should come from environment or secrets manager
}
}
環境変数
// 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 との統合
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{vaultName}.vault.azure.net/"),
new DefaultAzureCredential()
);
セキュリティイベントのログ記録
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!
}
アンチパターン
| アンチパターン | 問題点 | 正しいアプローチ |
|---|---|---|
| SQL での文字列補間 | SQL インジェクション | パラメーター化されたクエリを使用する |
Html.Raw(userInput) | XSS の脆弱性 | デフォルトのエンコードを使用する |
| appsettings にシークレットを保存する | シークレットの漏えい | User Secrets/Key Vault を使用する |
あらゆる場所で [AllowAnonymous] を指定する | 認証されない | 選択的に適用する |
| HTTPS リダイレクトを無効にする | 中間者攻撃 | HTTPS を有効に保つ |
| 独自の暗号化を実装する | 暗号化が弱い | 組み込みライブラリを使用する |
| すべての例外を捕捉する | セキュリティ上の問題が隠れる | ログに記録し、個別に処理する |
クイックトラブルシューティング
| 問題 | 考えられる原因 | 解決策 |
|---|---|---|
| 401 Unauthorized | JWT の検証に失敗した | 発行者、対象者、キーを確認する |
| CORS エラー | オリジンが許可されていない | CORS ポリシーにオリジンを追加する |
| レート制限に達した | リクエストが多すぎる | レートリミッターの設定を調整する |
| パスワードの検証に失敗する | ポリシーの要件 | Identity のパスワードオプションを確認する |
| NuGet の脆弱性 | パッケージが古い | 修正済みバージョンに更新する |
| User Secrets が読み込まれない | Development 環境ではない | ASPNETCORE_ENVIRONMENT を確認する |
セキュリティスキャンのコマンド
# 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 .
関連スキル
コメント
Markdown 対応