From 3e2f161ba59625bdce2f9101e299b490ee72411f Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Fri, 3 Apr 2026 22:44:15 +0200 Subject: [PATCH] Add security headers, CSP with nonces, and harden input handling - Add Content-Security-Policy with per-request cryptographic nonces for all inline Blazor scripts (DarkModeScript, InertiaShell importmap, AppLayout, PublicLayout, OAuthCallback) - Add X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and X-Permitted-Cross-Domain-Policies response headers - Replace innerHTML message interpolation with textContent in error toast to eliminate XSS vector in app.tsx - Harden XML parsing in InstalledPackageDetector with DtdProcessing.Prohibit and null XmlResolver to prevent XXE - Switch InstallCommand from string-concatenated Arguments to ArgumentList for safe process argument passing - Add production warning log when seed users are created with default passwords outside Development environment --- .../Commands/Install/InstallCommand.cs | 27 ++++++++-------- .../Components/DarkModeScript.razor | 5 ++- .../Components/InertiaShell.razor | 4 ++- .../Components/Layout/AppLayout.razor | 4 ++- .../Components/Layout/PublicLayout.razor | 4 ++- .../SimpleModule.Core/Security/CspNonce.cs | 8 +++++ .../SimpleModule.Core/Security/ICspNonce.cs | 6 ++++ .../SimpleModuleHostExtensions.cs | 31 +++++++++++++++++++ .../InstalledPackageDetector.cs | 8 ++++- .../Components/Pages/OAuthCallback.razor | 4 ++- .../Services/UserSeedService.cs | 13 ++++++++ template/SimpleModule.Host/ClientApp/app.tsx | 3 +- 12 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 framework/SimpleModule.Core/Security/CspNonce.cs create mode 100644 framework/SimpleModule.Core/Security/ICspNonce.cs diff --git a/cli/SimpleModule.Cli/Commands/Install/InstallCommand.cs b/cli/SimpleModule.Cli/Commands/Install/InstallCommand.cs index a5f96092..f9e3da8a 100644 --- a/cli/SimpleModule.Cli/Commands/Install/InstallCommand.cs +++ b/cli/SimpleModule.Cli/Commands/Install/InstallCommand.cs @@ -31,23 +31,24 @@ public override int Execute(CommandContext context, InstallSettings settings) $"Installing [green]{Markup.Escape(settings.PackageId)}[/] into [blue]{Markup.Escape(Path.GetFileName(hostCsproj))}[/]..." ); - var args = $"add \"{hostCsproj}\" package {settings.PackageId}"; + var psi = new ProcessStartInfo + { + FileName = "dotnet", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + psi.ArgumentList.Add("add"); + psi.ArgumentList.Add(hostCsproj); + psi.ArgumentList.Add("package"); + psi.ArgumentList.Add(settings.PackageId); if (!string.IsNullOrWhiteSpace(settings.Version)) { - args += $" --version {settings.Version}"; + psi.ArgumentList.Add("--version"); + psi.ArgumentList.Add(settings.Version); } - using var process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = "dotnet", - Arguments = args, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - }, - }; + using var process = new Process { StartInfo = psi }; process.Start(); var outputTask = process.StandardOutput.ReadToEndAsync(); diff --git a/framework/SimpleModule.Blazor/Components/DarkModeScript.razor b/framework/SimpleModule.Blazor/Components/DarkModeScript.razor index 81166e64..857ebe74 100644 --- a/framework/SimpleModule.Blazor/Components/DarkModeScript.razor +++ b/framework/SimpleModule.Blazor/Components/DarkModeScript.razor @@ -1,4 +1,7 @@ - - diff --git a/modules/Users/src/SimpleModule.Users/Services/UserSeedService.cs b/modules/Users/src/SimpleModule.Users/Services/UserSeedService.cs index c20bafaa..e42b05b2 100644 --- a/modules/Users/src/SimpleModule.Users/Services/UserSeedService.cs +++ b/modules/Users/src/SimpleModule.Users/Services/UserSeedService.cs @@ -11,6 +11,7 @@ namespace SimpleModule.Users.Services; public partial class UserSeedService( IServiceProvider serviceProvider, IConfiguration configuration, + IHostEnvironment environment, ILogger logger ) : IHostedService { @@ -116,6 +117,8 @@ string role }; var password = configuration[passwordConfigKey] ?? defaultPassword; + if (password == defaultPassword && !environment.IsDevelopment()) + LogDefaultPasswordWarning(logger, email, passwordConfigKey); var result = await userManager.CreateAsync(user, password); if (result.Succeeded) { @@ -136,6 +139,16 @@ string role [LoggerMessage(Level = LogLevel.Information, Message = "Seeding user: {Email}")] private static partial void LogSeedingUser(ILogger logger, string email); + [LoggerMessage( + Level = LogLevel.Warning, + Message = "Seeding {Email} with default password. Set '{ConfigKey}' in configuration before deploying to production." + )] + private static partial void LogDefaultPasswordWarning( + ILogger logger, + string email, + string configKey + ); + [LoggerMessage(Level = LogLevel.Error, Message = "Seed error: {ErrorDescription}")] private static partial void LogSeedError(ILogger logger, string errorDescription); } diff --git a/template/SimpleModule.Host/ClientApp/app.tsx b/template/SimpleModule.Host/ClientApp/app.tsx index d8e6dbc9..13948823 100644 --- a/template/SimpleModule.Host/ClientApp/app.tsx +++ b/template/SimpleModule.Host/ClientApp/app.tsx @@ -119,13 +119,14 @@ function showErrorToast(message: string) {

Error

-

${message.replace(/[<>"'&]/g, (c) => `&#${c.charCodeAt(0)};`)}

+

`; + container.querySelector('p.opacity-90')!.textContent = message; container.querySelector('button')?.addEventListener('click', () => container.remove()); document.body.appendChild(container); setTimeout(() => container.remove(), 8000);