dotnet-security-owasp について
dotnet-security-owasp は、.NET 開発者向けのセキュリティガイダンススキルです。OWASP Top 10(2021)に沿って、.NET 固有のリスク、緩和例、よくある落とし穴をまとめています。
主な機能
- 01OWASP 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、パスキー、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] 属性がないこと、所有権の検証を行わずにルートパラメーターからユーザー ID を取得する安全でない直接オブジェクト参照(IDOR)、意図しないオリジンを許可する 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();
落とし穴: 正しく適用するには、UseRateLimiter() を UseRouting() の後、MapControllers()/MapGet() の前に呼び出す必要があります。
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 パスワードポリシーが弱すぎること、Secure/SameSite 属性のないセッション Cookie、アカウントロックアウト設定の不備です。
緩和策
// 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}")を使うと構造化ログが回避され、マスキングに対応していないログの出力先に個人識別情報(PII)が漏れる可能性があります。
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)
CAS は .NET Core/.NET 5 以降ではサポートされていません。CAS を目的として System.Security.Permissions、SecurityPermission、または [SecurityCritical]/[SecuritySafeCritical] 属性を参照するコードは、削除するか、OS レベルのセキュリティ境界(コンテナー、プロセス分離)に置き換える必要があります。
部分的に信頼された呼び出し元を許可(APTCA)
[AllowPartiallyTrustedCallers] 属性は、.NET Core/.NET 5 以降では効果がありません。部分的信頼のモデルは廃止されています。移行時に APTCA 属性を削除してください。代わりに標準的な認可と入力検証を使用してください。
.NET Remoting
.NET Remoting は .NET Core/.NET 5 以降では利用できません。リモートオブジェクトのデシリアライズに制限がなかったため、本質的に安全ではありませんでした。次のものに置き換えてください。
- プロセス間/マシン間の RPC には gRPC(トランスポートのセキュリティについては [skill:dotnet-cryptography] を参照)
- 同一マシン内の IPC には名前付きパイプ
- サービス間通信には HTTP API
DCOM
Distributed COM(DCOM)はWindows 専用で、.NET Core/.NET 5 以降ではサポートされていません。分散通信には gRPC、REST API、またはメッセージキューに置き換えてください。
BinaryFormatter
BinaryFormatter は .NET 8 でエラー扱いで非推奨(SYSLIB0011)となり、.NET 9 以降では削除されています。デシリアライズ攻撃によって任意のコードが実行されるおそれがあります。次のものに置き換えてください。
- JSON のシリアル化には
System.Text.Json - バイナリ形式には MessagePack または Protocol Buffers
- XML の用途には、許可する型を厳密に限定した
XmlSerializer
回避策として System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization を true に設定しないでください。
エージェントに関する注意点
- 明確な理由なく
[AllowAnonymous]を使用しないでください。これはグローバルなフォールバックポリシーを上書きします。匿名アクセスを許可する各エンドポイントには、その理由を説明するコメントを付けてください。 - 手軽さを理由に HTTPS リダイレクトを無効にしないでください。ローカル開発では代わりに
dotnet dev-certs https --trustを使用してください。 - リクエストボディをそのままログに記録しないでください。認証情報、トークン、個人識別情報(PII)が含まれている可能性があります。安全なフィールドを選択するには
HttpLoggingFieldsを使用してください。 - クライアント側の検証だけに頼らないでください。必ずサーバー側でも検証してください。Razor のフォーム検証は UX のためのものであり、セキュリティ対策ではありません。
- 文字列補間を使って
FromSqlRawを使用しないでください。自動でパラメーター化されるFromSqlInterpolatedを使用してください。 - シークレットを
appsettings.jsonに保存しないでください。開発ではユーザーシークレットを、本番環境では環境変数またはマネージド ID を使用してください。[skill:dotnet-secrets-management] を参照してください。 - 非推奨のパターンを使ってセキュリティ上重要なコードを生成しないでください。CAS、APTCA、.NET Remoting、DCOM、BinaryFormatter はすべて、最新の .NET ではサポートされていません。上記の「非推奨のセキュリティパターン」セクションを参照してください。
前提条件
- .NET 8.0 以降(LTS の基準バージョン)
- セキュリティミドルウェア、CSRF 対策、レート制限を利用する場合は ASP.NET Core 8.0 以降
- 認証/ID 管理に Microsoft.AspNetCore.Identity を使用する場合(A07 パターンを使用する場合)