From aa62e528e08236cdde1fbf944f14ed0efe476573 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Mon, 4 May 2026 21:10:42 +0100 Subject: [PATCH 1/3] Upgrade security policies --- .editorconfig | 2 +- .github/copilot-instructions.md | 35 ++++++++ .github/workflows/GithubActionsRelease.yml | 2 +- .github/workflows/GithubActionsWIP.yml | 2 +- .markdownlint.json | 10 +++ .prettierrc.json | 18 ++++ .vscode/extensions.json | 12 +++ .vscode/launch.json | 26 ++++++ .vscode/settings.json | 30 +++++++ .vscode/tasks.json | 18 ++++ CLAUDE.md | 14 +++ Directory.Build.props | 5 ++ HttpSecurity.AspNet.sln | 48 ---------- HttpSecurity.AspNet.slnx | 5 ++ .../HttpSecurity.AspNet.csproj | 5 +- .../HttpSecurity.Example.csproj | 3 +- HttpSecurity.Example/Program.cs | 6 +- .../Properties/launchSettings.json | 4 +- HttpSecurity.Example/appsettings.json | 1 + SourceGenerator/SourceGenerator.cs | 87 ++++++------------- SourceGenerator/SourceGenerator.csproj | 6 +- dotnet10.sln | 36 ++++++++ global.json | 7 ++ 23 files changed, 259 insertions(+), 123 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .markdownlint.json create mode 100644 .prettierrc.json create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 CLAUDE.md create mode 100644 Directory.Build.props delete mode 100644 HttpSecurity.AspNet.sln create mode 100644 HttpSecurity.AspNet.slnx create mode 100644 dotnet10.sln create mode 100644 global.json diff --git a/.editorconfig b/.editorconfig index 4c88e6b..3e3a11b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -53,7 +53,7 @@ dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_style_operator_placement_when_wrapping = beginning_of_line tab_width = 4 indent_size = 4 -end_of_line = crlf +end_of_line = lf dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..fd64c20 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,35 @@ +# HttpSecurity.AspNet — AI Development Guide + +> Open-source ASP.NET class library for HTTP security headers and Content Security Policy + +--- + +## Quick Reference + +| Item | Value | +| :--------------- | :------------------------------------------------- | +| **Framework** | .NET 8.0 (`net8.0`) — runs on .NET 8, 9, 10 | +| **Build** | `dotnet build HttpSecurity.AspNet.slnx` | +| **Pack** | `dotnet pack HttpSecurity.AspNet/HttpSecurity.AspNet.csproj` | +| **Source gen** | `SourceGenerator/` targets `netstandard2.0` | + +--- + +## Project Architecture + +| Project | Purpose | TFM | +| :----------------------- | :------------------------------------------------- | :---------------- | +| **HttpSecurity.AspNet** | Library: CSP, headers, SRI hash generation | `net8.0` | +| **SourceGenerator** | Roslyn source generator for hash computation | `netstandard2.0` | +| **HttpSecurity.Example** | Example Blazor Server app | `net8.0` | + +--- + +## C# Standards + +- `ImplicitUsings=enable`, `Nullable=enable` +- Interfaces: `I` prefix +- Types, properties, methods: PascalCase +- File-scoped namespaces +- Expression-bodied members where appropriate +- Null-coalescing (`??`) and null-propagation (`?.`) preferred diff --git a/.github/workflows/GithubActionsRelease.yml b/.github/workflows/GithubActionsRelease.yml index 5aaeed7..d601d03 100644 --- a/.github/workflows/GithubActionsRelease.yml +++ b/.github/workflows/GithubActionsRelease.yml @@ -62,7 +62,7 @@ jobs: - name: Use dotnet uses: actions/setup-dotnet@v3 with: - dotnet-version: '8.x' + dotnet-version: '10.x' - name: Build HttpSecurity.AspNet 🔧 run: dotnet build ${{env.projectCSFB}} --configuration ${{env.buildConfiguration}} -p:Version=${{env.version}} diff --git a/.github/workflows/GithubActionsWIP.yml b/.github/workflows/GithubActionsWIP.yml index b87097a..3c1d841 100644 --- a/.github/workflows/GithubActionsWIP.yml +++ b/.github/workflows/GithubActionsWIP.yml @@ -57,7 +57,7 @@ jobs: - name: Use dotnet uses: actions/setup-dotnet@v3 with: - dotnet-version: '8.x' + dotnet-version: '10.x' - name: Build HttpSecurity.AspNet 🔧 run: dotnet build ${{env.projectCSFB}} --configuration ${{env.buildConfiguration}} --version-suffix ${{env.ciSuffix}} diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..ba25a61 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,10 @@ +{ + "MD013": { + "line_length": 180, + "code_blocks": false, + "tables": false + }, + "MD024": false, + "MD033": false, + "MD041": false +} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..aa44f8b --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,18 @@ +{ + "endOfLine": "lf", + "tabWidth": 2, + "useTabs": false, + "singleQuote": false, + "proseWrap": "preserve", + "printWidth": 240, + "overrides": [ + { + "files": "*.md", + "options": { + "parser": "markdown", + "printWidth": 180, + "proseWrap": "always" + } + } + ] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c995e53 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,12 @@ +{ + "recommendations": [ + "EditorConfig.EditorConfig", + "esbenp.prettier-vscode", + "DavidAnson.vscode-markdownlint", + "bierner.markdown-mermaid", + "GitHub.copilot-chat", + "ms-dotnettools.csdevkit", + "ms-dotnettools.csharp", + "ms-dotnettools.vscode-dotnet-runtime" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..16c899a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "HttpSecurity.Example", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build HttpSecurity.Example", + "program": "${workspaceFolder}/.artifacts/bin/HttpSecurity.Example/debug/HttpSecurity.Example.dll", + "cwd": "${workspaceFolder}/HttpSecurity.Example", + "stopAtEntry": false, + "justMyCode": true, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "https://localhost:50083" + }, + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c4ca9fd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "extensions.experimental.affinity": { + "GitHub.copilot": 1, + "GitHub.copilot-chat": 1 + }, + "github.copilot.editor.enableAutoCompletions": true, + "files.exclude": { + "**/node_modules": true, + "**/bin": true, + "**/obj": true, + "**/.git": true, + "**/.artifacts": true + }, + "search.exclude": { + "**/node_modules": true, + "**/bin": true, + "**/obj": true, + "**/.artifacts": true + }, + "editor.formatOnSave": true, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.wordWrap": "wordWrapColumn", + "editor.wordWrapColumn": 180 + }, + "files.associations": { + "appsettings*.json": "jsonc" + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..e1fe801 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build HttpSecurity.Example", + "type": "shell", + "command": "dotnet", + "args": ["build", "--project", "${workspaceFolder}/HttpSecurity.Example/HttpSecurity.Example.csproj"], + "presentation": { + "reveal": "silent", + "panel": "dedicated", + "clear": false + }, + "problemMatcher": "$msCompile", + "group": "build" + } + ] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c04706d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,14 @@ +# HttpSecurity.AspNet — Claude Code Instructions + +> Open-source ASP.NET class library for HTTP security headers and Content Security Policy + +## Development Guide + +Read and follow all rules in `.github/copilot-instructions.md` — it is the single source of truth for project conventions, coding standards, and architecture. + +## Available Commands + +| Command | Purpose | +| :--------------------------------------------------------------- | :----------------- | +| `dotnet build HttpSecurity.AspNet.slnx` | Build solution | +| `dotnet pack HttpSecurity.AspNet/HttpSecurity.AspNet.csproj` | Create NuGet package | diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..9b95ed0 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,5 @@ + + + $(MSBuildThisFileDirectory).artifacts + + diff --git a/HttpSecurity.AspNet.sln b/HttpSecurity.AspNet.sln deleted file mode 100644 index 7241b28..0000000 --- a/HttpSecurity.AspNet.sln +++ /dev/null @@ -1,48 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.4.32912.340 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGenerator", "SourceGenerator\SourceGenerator.csproj", "{47D7D5B5-BBBD-4CD5-9FC3-4405EA55FD08}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpSecurity.Example", "HttpSecurity.Example\HttpSecurity.Example.csproj", "{7FA94ABA-D9D6-4501-A002-72CA6D1C3BC3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A60737BC-8E5F-4B2E-BCFC-793BD95DFC14}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - .gitattributes = .gitattributes - .gitignore = .gitignore - .github\dependabot.yml = .github\dependabot.yml - .github\workflows\GithubActionsRelease.yml = .github\workflows\GithubActionsRelease.yml - .github\workflows\GithubActionsWIP.yml = .github\workflows\GithubActionsWIP.yml - README.md = README.md - ReleaseNotes.md = ReleaseNotes.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpSecurity.AspNet", "HttpSecurity.AspNet\HttpSecurity.AspNet.csproj", "{DD306EF6-02EC-4DE2-A0AA-C5325E596F00}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {47D7D5B5-BBBD-4CD5-9FC3-4405EA55FD08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {47D7D5B5-BBBD-4CD5-9FC3-4405EA55FD08}.Debug|Any CPU.Build.0 = Debug|Any CPU - {47D7D5B5-BBBD-4CD5-9FC3-4405EA55FD08}.Release|Any CPU.ActiveCfg = Release|Any CPU - {47D7D5B5-BBBD-4CD5-9FC3-4405EA55FD08}.Release|Any CPU.Build.0 = Release|Any CPU - {7FA94ABA-D9D6-4501-A002-72CA6D1C3BC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7FA94ABA-D9D6-4501-A002-72CA6D1C3BC3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7FA94ABA-D9D6-4501-A002-72CA6D1C3BC3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7FA94ABA-D9D6-4501-A002-72CA6D1C3BC3}.Release|Any CPU.Build.0 = Release|Any CPU - {DD306EF6-02EC-4DE2-A0AA-C5325E596F00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DD306EF6-02EC-4DE2-A0AA-C5325E596F00}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DD306EF6-02EC-4DE2-A0AA-C5325E596F00}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DD306EF6-02EC-4DE2-A0AA-C5325E596F00}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {DF4D493C-EC0A-489A-B4C3-DDCF33178524} - EndGlobalSection -EndGlobal diff --git a/HttpSecurity.AspNet.slnx b/HttpSecurity.AspNet.slnx new file mode 100644 index 0000000..1d1dc05 --- /dev/null +++ b/HttpSecurity.AspNet.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/HttpSecurity.AspNet/HttpSecurity.AspNet.csproj b/HttpSecurity.AspNet/HttpSecurity.AspNet.csproj index c627e7c..10168e8 100644 --- a/HttpSecurity.AspNet/HttpSecurity.AspNet.csproj +++ b/HttpSecurity.AspNet/HttpSecurity.AspNet.csproj @@ -2,7 +2,6 @@ net8.0 - 11 enable enable README.md @@ -10,8 +9,8 @@ - - + + diff --git a/HttpSecurity.Example/HttpSecurity.Example.csproj b/HttpSecurity.Example/HttpSecurity.Example.csproj index 965ef17..b9e6613 100644 --- a/HttpSecurity.Example/HttpSecurity.Example.csproj +++ b/HttpSecurity.Example/HttpSecurity.Example.csproj @@ -1,9 +1,10 @@  - net8.0 + net10.0 enable enable + true diff --git a/HttpSecurity.Example/Program.cs b/HttpSecurity.Example/Program.cs index 993d894..206e9e0 100644 --- a/HttpSecurity.Example/Program.cs +++ b/HttpSecurity.Example/Program.cs @@ -40,7 +40,7 @@ .AddFormAction(o => o.AddNone()) .AddImgSrc(o => o - .AddSelf() + .AddSelf() .AddUri("www.google-analytics.com") .AddUri("*.openstreetmap.org") .AddSchemeSource(SchemeSource.Data, "w3.org/svg/2000")) @@ -112,10 +112,10 @@ app.UseHttpSecurityHeaders(); -app.UseStaticFiles(); - app.UseRouting(); +app.MapStaticAssets(); + app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); diff --git a/HttpSecurity.Example/Properties/launchSettings.json b/HttpSecurity.Example/Properties/launchSettings.json index b7eca4f..8c25c30 100644 --- a/HttpSecurity.Example/Properties/launchSettings.json +++ b/HttpSecurity.Example/Properties/launchSettings.json @@ -8,7 +8,7 @@ }, "hotReloadEnabled": false, "dotnetRunMessages": true, - "applicationUrl": "https://localhost:50083", + "applicationUrl": "https://localhost:50083" } } -} \ No newline at end of file +} diff --git a/HttpSecurity.Example/appsettings.json b/HttpSecurity.Example/appsettings.json index 10f68b8..f8930b6 100644 --- a/HttpSecurity.Example/appsettings.json +++ b/HttpSecurity.Example/appsettings.json @@ -1,4 +1,5 @@ { + "ReloadStaticAssetsAtRuntime": false, "Logging": { "LogLevel": { "Default": "Information", diff --git a/SourceGenerator/SourceGenerator.cs b/SourceGenerator/SourceGenerator.cs index e86a841..ca07558 100644 --- a/SourceGenerator/SourceGenerator.cs +++ b/SourceGenerator/SourceGenerator.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Generic; +using System.Collections.Immutable; #if DEBUG && GENERATOR_DEBUG using System.Diagnostics; #endif @@ -11,9 +12,9 @@ namespace HttpSecurity.AspNet; [Generator] -internal class SourceGenerator : ISourceGenerator +internal class SourceGenerator : IIncrementalGenerator { - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { #if DEBUG && GENERATOR_DEBUG if (!Debugger.IsAttached) @@ -22,33 +23,31 @@ public void Initialize(GeneratorInitializationContext context) } #endif - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + var classDeclarations = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (node, _) => node is ClassDeclarationSyntax, + transform: static (ctx, _) => (ClassDeclarationSyntax)ctx.Node) + .Collect(); + + var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations); + + context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => + Execute(source.Left, source.Right, spc)); } - private readonly string[] _policyOptionAdditionalAttributes = { "AddNone", "AddReportSample", "AddScript", "AddSelf", "AddStrictDynamic", "AddUnsafeEval", "AddUnsafeHashes", "AddUnsafeInline" }; + private static readonly string[] _policyOptionAdditionalAttributes = { "AddNone", "AddReportSample", "AddScript", "AddSelf", "AddStrictDynamic", "AddUnsafeEval", "AddUnsafeHashes", "AddUnsafeInline" }; - public void Execute(GeneratorExecutionContext context) + private static void Execute(Compilation compilation, ImmutableArray classes, SourceProductionContext context) { Extensions.LinesGenerated = 0; - // retreive the populated receiver - if (context.SyntaxReceiver is not SyntaxReceiver receiver) - { - return; - } - - // we're going to create a new compilation that contains the attribute. - // TODO: we should allow source generators to provide source during initialize, so that this step isn't required. - //CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions; - Compilation compilation = context.Compilation; - List policyClassSymbols = new(); Dictionary policyOptionClassSymbols = new(); INamedTypeSymbol contentSecurityPolicyOptionsClassSymbol = null; - foreach (var classNode in receiver.Classes) + foreach (var classNode in classes) { var modifiers = classNode.Modifiers.Select(m => m.Text).ToList(); SemanticModel classModel = compilation.GetSemanticModel(classNode.SyntaxTree); @@ -110,7 +109,7 @@ public void Execute(GeneratorExecutionContext context) } - private StringBuilder ProcessContentSecurityPolicyOptions(INamedTypeSymbol contentSecurityPolicyOptionsClassSymbol, List policyClassSymbols, Dictionary policyOptionClassSymbols) + private static StringBuilder ProcessContentSecurityPolicyOptions(INamedTypeSymbol contentSecurityPolicyOptionsClassSymbol, List policyClassSymbols, Dictionary policyOptionClassSymbols) { StringBuilder sb = new(); var isFirst = true; @@ -174,7 +173,7 @@ private StringBuilder ProcessContentSecurityPolicyOptions(INamedTypeSymbol conte } - private bool ProcessPolicyAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) + private static bool ProcessPolicyAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) { var attribute = classSymbol.GetAttributes().Where(ad => ad.AttributeClass.Name == $"ContentSecurityPolicyAttribute").FirstOrDefault(); @@ -219,7 +218,7 @@ private bool ProcessPolicyAttribute(INamedTypeSymbol classSymbol, StringBuilder } - private bool ProcessPolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) + private static bool ProcessPolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) { var attribute = classSymbol.GetAttributes().Where(ad => ad.AttributeClass.Name == $"ContentSecurityPolicyOptionsAttribute").FirstOrDefault(); @@ -256,7 +255,7 @@ private bool ProcessPolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringB } - private bool ProcessAdditionalPolicyOptionsAttribute(INamedTypeSymbol classSymbol, string attributeName, StringBuilder sb) + private static bool ProcessAdditionalPolicyOptionsAttribute(INamedTypeSymbol classSymbol, string attributeName, StringBuilder sb) { var attribute = classSymbol.GetAttributes().Where(ad => ad.AttributeClass.Name == $"{GetLongAttributeName(attributeName)}").FirstOrDefault(); @@ -298,7 +297,7 @@ private bool ProcessAdditionalPolicyOptionsAttribute(INamedTypeSymbol classSymbo } - private bool ProcessGroupNamePolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) + private static bool ProcessGroupNamePolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) { var attribute = classSymbol.GetAttributes().Where(ad => ad.AttributeClass.Name == $"{GetLongAttributeName("AddGroupName")}").FirstOrDefault(); @@ -338,7 +337,7 @@ private bool ProcessGroupNamePolicyOptionsAttribute(INamedTypeSymbol classSymbol } - private bool ProcessHashValuePolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) + private static bool ProcessHashValuePolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) { var attribute = classSymbol.GetAttributes().Where(ad => ad.AttributeClass.Name == $"{GetLongAttributeName("AddHashValue")}").FirstOrDefault(); @@ -416,7 +415,7 @@ private bool ProcessHashValuePolicyOptionsAttribute(INamedTypeSymbol classSymbol } - private bool ProcessHostSourcePolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) + private static bool ProcessHostSourcePolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) { var attribute = classSymbol.GetAttributes().Where(ad => ad.AttributeClass.Name == $"{GetLongAttributeName("AddHostSourceValue")}").FirstOrDefault(); @@ -456,7 +455,7 @@ private bool ProcessHostSourcePolicyOptionsAttribute(INamedTypeSymbol classSymbo } - private bool ProcessNoncePolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) + private static bool ProcessNoncePolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) { var attribute = classSymbol.GetAttributes().Where(ad => ad.AttributeClass.Name == $"{GetLongAttributeName("AddNonce")}").FirstOrDefault(); @@ -496,7 +495,7 @@ private bool ProcessNoncePolicyOptionsAttribute(INamedTypeSymbol classSymbol, St } - private bool ProcessPolicyNamePolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) + private static bool ProcessPolicyNamePolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) { var attribute = classSymbol.GetAttributes().Where(ad => ad.AttributeClass.Name == $"{GetLongAttributeName("AddPolicyName")}").FirstOrDefault(); @@ -536,7 +535,7 @@ private bool ProcessPolicyNamePolicyOptionsAttribute(INamedTypeSymbol classSymbo } - private bool ProcessSchemeSourcePolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) + private static bool ProcessSchemeSourcePolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) { var attribute = classSymbol.GetAttributes().Where(ad => ad.AttributeClass.Name == $"{GetLongAttributeName("AddSchemeSource")}").FirstOrDefault(); @@ -604,7 +603,7 @@ private bool ProcessSchemeSourcePolicyOptionsAttribute(INamedTypeSymbol classSym } - private bool ProcessUriPolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) + private static bool ProcessUriPolicyOptionsAttribute(INamedTypeSymbol classSymbol, StringBuilder sb) { var attribute = classSymbol.GetAttributes().Where(ad => ad.AttributeClass.Name == $"{GetLongAttributeName("AddUri")}").FirstOrDefault(); @@ -671,38 +670,6 @@ private bool ProcessUriPolicyOptionsAttribute(INamedTypeSymbol classSymbol, Stri } - /// - /// Created on demand before each generation pass - /// - class SyntaxReceiver : ISyntaxReceiver - { - ///// - ///// Dictionary keyed by class nodes that have a ViewModelRecord attribute and with value being a list of - ///// properties with the ViewModelProperty attribute in that record. - ///// - //public readonly Dictionary> ClassNodes = new(); - - - /// - /// List of classes with the ViewModelRecord attribute. Diagnostic reporting will be created for these classes - /// because the attribute is for partial records only. - /// - public readonly List Classes = new(); - - - /// - /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation - /// - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - // any field with at least one attribute is a candidate for property generation - if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax) - { - Classes.Add(classDeclarationSyntax); - } - } - } - private static string GetClassTypeName(INamedTypeSymbol classSymbol, bool suppressGeneric = false) { diff --git a/SourceGenerator/SourceGenerator.csproj b/SourceGenerator/SourceGenerator.csproj index def7e04..26472e1 100644 --- a/SourceGenerator/SourceGenerator.csproj +++ b/SourceGenerator/SourceGenerator.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 11 + latest true @@ -13,11 +13,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/dotnet10.sln b/dotnet10.sln new file mode 100644 index 0000000..7698312 --- /dev/null +++ b/dotnet10.sln @@ -0,0 +1,36 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpSecurity.Example", "HttpSecurity.Example\HttpSecurity.Example.csproj", "{1A25AA86-DDFB-38A7-3371-7793AC13C922}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGenerator", "SourceGenerator\SourceGenerator.csproj", "{DA39BAE9-51E6-F712-D052-5DF9B72D3A9A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpSecurity.AspNet", "HttpSecurity.AspNet\HttpSecurity.AspNet.csproj", "{6704D854-500D-5569-047F-126DD5B5C9C5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1A25AA86-DDFB-38A7-3371-7793AC13C922}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A25AA86-DDFB-38A7-3371-7793AC13C922}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A25AA86-DDFB-38A7-3371-7793AC13C922}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A25AA86-DDFB-38A7-3371-7793AC13C922}.Release|Any CPU.Build.0 = Release|Any CPU + {DA39BAE9-51E6-F712-D052-5DF9B72D3A9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA39BAE9-51E6-F712-D052-5DF9B72D3A9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA39BAE9-51E6-F712-D052-5DF9B72D3A9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA39BAE9-51E6-F712-D052-5DF9B72D3A9A}.Release|Any CPU.Build.0 = Release|Any CPU + {6704D854-500D-5569-047F-126DD5B5C9C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6704D854-500D-5569-047F-126DD5B5C9C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6704D854-500D-5569-047F-126DD5B5C9C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6704D854-500D-5569-047F-126DD5B5C9C5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0714180F-EBEB-417C-9AF6-299BE531DBE0} + EndGlobalSection +EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 0000000..53b4c27 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.103", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} From e07d0ccf88846392f83ad4938d226e16c6ea9115 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Mon, 4 May 2026 21:15:09 +0100 Subject: [PATCH 2/3] Update packages --- .github/workflows/GithubActionsRelease.yml | 94 +++++++++---------- .github/workflows/GithubActionsWIP.yml | 65 +++++++------ .../HttpSecurity.AspNet.csproj | 4 +- SourceGenerator/SourceGenerator.csproj | 4 +- 4 files changed, 82 insertions(+), 85 deletions(-) diff --git a/.github/workflows/GithubActionsRelease.yml b/.github/workflows/GithubActionsRelease.yml index d601d03..06b8ad6 100644 --- a/.github/workflows/GithubActionsRelease.yml +++ b/.github/workflows/GithubActionsRelease.yml @@ -11,38 +11,37 @@ on: push: tags: - - '*' # Push events to matching *, i.e. 1.0, 20.15.10 + - "*" # Push events to matching *, i.e. 1.0, 20.15.10 env: - buildPlatform: 'Any CPU' - buildConfiguration: 'Release' + buildPlatform: "Any CPU" + buildConfiguration: "Release" outputCSFB: ${{github.workspace}}\siteCSFB - projectCSFB: 'HttpSecurity.AspNet/HttpSecurity.AspNet.csproj' - -jobs: + projectCSFB: "HttpSecurity.AspNet/HttpSecurity.AspNet.csproj" -############################################################################################################ -# These jobs are used to gate actions. By creating these jobs we don't need to proliferate the repo checks -############################################################################################################ +jobs: + ############################################################################################################ + # These jobs are used to gate actions. By creating these jobs we don't need to proliferate the repo checks + ############################################################################################################ is-on-fork: name: Running on a fork? runs-on: ubuntu-latest if: github.repository != 'Material-Blazor/HttpSecurity.AspNet' steps: - - name: Nothing to see here - run: echo "" + - name: Nothing to see here + run: echo "" is-on-material-blazor: name: Running on Material-Blazor/HttpSecurity.AspNet? runs-on: ubuntu-latest if: github.repository == 'Material-Blazor/HttpSecurity.AspNet' steps: - - name: Nothing to see here - run: echo "" + - name: Nothing to see here + run: echo "" -############################################################################################################ -# Build package and deploy -############################################################################################################ + ############################################################################################################ + # Build package and deploy + ############################################################################################################ build-and-deploy-package: name: Build nuget package & deploy to nuget needs: [is-on-material-blazor] @@ -50,36 +49,35 @@ jobs: runs-on: windows-latest steps: - - name: Get the version - run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV - shell: bash - - - name: Checkout repository under $GITHUB_WORKSPACE so the job can access it 🛎️ - uses: actions/checkout@v4 - with: - persist-credentials: false + - name: Get the version + run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV + shell: bash - - name: Use dotnet - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '10.x' + - name: Checkout repository under $GITHUB_WORKSPACE so the job can access it 🛎️ + uses: actions/checkout@v6 + with: + persist-credentials: false - - name: Build HttpSecurity.AspNet 🔧 - run: dotnet build ${{env.projectCSFB}} --configuration ${{env.buildConfiguration}} -p:Version=${{env.version}} + - name: Use dotnet + uses: actions/setup-dotnet@v5 + with: + dotnet-version: "10.x" - - name: Generate the NuGet package 🔧 - run: dotnet pack ${{env.projectCSFB}} --no-build --configuration ${{env.buildConfiguration}} --output ${{env.outputCSFB}} -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:Version=${{env.version}} + - name: Build HttpSecurity.AspNet 🔧 + run: dotnet build ${{env.projectCSFB}} --configuration ${{env.buildConfiguration}} -p:Version=${{env.version}} - - name: Display HttpSecurity.AspNet package output Ꙫ - run: dir ${{env.outputCSFB}} + - name: Generate the NuGet package 🔧 + run: dotnet pack ${{env.projectCSFB}} --no-build --configuration ${{env.buildConfiguration}} --output ${{env.outputCSFB}} -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:Version=${{env.version}} - - name: Upload Package 🚀 - run: dotnet nuget push ${{env.outputCSFB}}\*.nupkg -k ${{secrets.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json + - name: Display HttpSecurity.AspNet package output Ꙫ + run: dir ${{env.outputCSFB}} + - name: Upload Package 🚀 + run: dotnet nuget push ${{env.outputCSFB}}\*.nupkg -k ${{secrets.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json -############################################################################################################ -# Create release -############################################################################################################ + ############################################################################################################ + # Create release + ############################################################################################################ create-release: name: Create release needs: [build-and-deploy-package, is-on-material-blazor] @@ -87,12 +85,12 @@ jobs: runs-on: ubuntu-latest steps: - - name: Get the version - run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV - shell: bash - - - name: Create Release - uses: ncipollo/release-action@v1 - with: - name: Release ${{env.version}} - tag: ${{env.version}} + - name: Get the version + run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV + shell: bash + + - name: Create Release + uses: ncipollo/release-action@v1.21.0 + with: + name: Release ${{env.version}} + tag: ${{env.version}} diff --git a/.github/workflows/GithubActionsWIP.yml b/.github/workflows/GithubActionsWIP.yml index 3c1d841..f9b0bf5 100644 --- a/.github/workflows/GithubActionsWIP.yml +++ b/.github/workflows/GithubActionsWIP.yml @@ -3,67 +3,66 @@ on: push: branches: - - 'main' + - "main" pull_request: branches: - - 'main' + - "main" env: - buildPlatform: 'Any CPU' - buildConfiguration: 'Release' + buildPlatform: "Any CPU" + buildConfiguration: "Release" outputCSFB: ${{github.workspace}}/siteCSFB - projectCSFB: 'HttpSecurity.AspNet/HttpSecurity.AspNet.csproj' + projectCSFB: "HttpSecurity.AspNet/HttpSecurity.AspNet.csproj" jobs: - -############################################################################################################ -# These jobs are used to gate actions. By creating these jobs we don't need to proliferate the repo checks -############################################################################################################ + ############################################################################################################ + # These jobs are used to gate actions. By creating these jobs we don't need to proliferate the repo checks + ############################################################################################################ is-on-fork: name: Running on a fork? runs-on: ubuntu-latest if: github.repository != 'Material-Blazor/HttpSecurity.AspNet' steps: - - name: Nothing to see here - run: echo "" + - name: Nothing to see here + run: echo "" is-on-material-blazor: name: Running on Material-Blazor/HttpSecurity.AspNet? runs-on: ubuntu-latest if: github.repository == 'Material-Blazor/HttpSecurity.AspNet' steps: - - name: Nothing to see here - run: echo "" + - name: Nothing to see here + run: echo "" -############################################################################################################ -# Build nuget package -############################################################################################################ + ############################################################################################################ + # Build nuget package + ############################################################################################################ build-package: name: Build nuget package runs-on: windows-latest steps: - - name: Set ciSuffix as env variable - run: echo "ciSuffix=ci.$(date +'%Y-%m-%d--%H%M')" >> $GITHUB_ENV - shell: bash + - name: Set ciSuffix as env variable + run: echo "ciSuffix=ci.$(date +'%Y-%m-%d--%H%M')" >> $GITHUB_ENV + shell: bash - - name: Checkout repository under $GITHUB_WORKSPACE so the job can access it 🛎️ - uses: actions/checkout@v4 - with: - persist-credentials: false + - name: Checkout repository under $GITHUB_WORKSPACE so the job can access it 🛎️ + uses: actions/checkout@v6 + with: + persist-credentials: false - - name: Use dotnet - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '10.x' + - name: Use dotnet + uses: actions/setup-dotnet@v5 + with: + dotnet-version: "10.x" - - name: Build HttpSecurity.AspNet 🔧 - run: dotnet build ${{env.projectCSFB}} --configuration ${{env.buildConfiguration}} --version-suffix ${{env.ciSuffix}} + - name: Build HttpSecurity.AspNet 🔧 + run: dotnet build ${{env.projectCSFB}} --configuration ${{env.buildConfiguration}} --version-suffix ${{env.ciSuffix}} - - name: Generate the NuGet package 🔧 - run: dotnet pack ${{env.projectCSFB}} --no-build --configuration ${{env.buildConfiguration}} --output ${{env.outputCSFB}} -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:Version=1.0.0-${{env.ciSuffix}} + - name: Generate the NuGet package 🔧 + run: dotnet pack ${{env.projectCSFB}} --no-build --configuration ${{env.buildConfiguration}} --output ${{env.outputCSFB}} -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:Version=1.0.0-${{env.ciSuffix}} - - name: Display HttpSecurity.AspNet package output Ꙫ - run: dir ${{env.outputCSFB}} + - name: Display HttpSecurity.AspNet package output Ꙫ + run: dir ${{env.outputCSFB}} diff --git a/HttpSecurity.AspNet/HttpSecurity.AspNet.csproj b/HttpSecurity.AspNet/HttpSecurity.AspNet.csproj index 10168e8..9a50a16 100644 --- a/HttpSecurity.AspNet/HttpSecurity.AspNet.csproj +++ b/HttpSecurity.AspNet/HttpSecurity.AspNet.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/SourceGenerator/SourceGenerator.csproj b/SourceGenerator/SourceGenerator.csproj index 26472e1..520422a 100644 --- a/SourceGenerator/SourceGenerator.csproj +++ b/SourceGenerator/SourceGenerator.csproj @@ -13,11 +13,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 209488b7f1c7c6795820b0c28f4a5f476bd3ccb4 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Mon, 4 May 2026 21:42:54 +0100 Subject: [PATCH 3/3] Upgrade security posture --- .../BlockAllMixedContent.cs | 6 +- .../ContentSecurityPolicies/NavigateTo.cs | 6 +- .../CrossOriginEmbedderPolicyDirective.cs | 22 ++++ .../CrossOriginOpenerPolicyDirective.cs | 22 ++++ .../CrossOriginResourcePolicyDirective.cs | 22 ++++ .../Enumerations/SchemeSource.cs | 12 ++ .../Services/HttpSecurityOptions.cs | 106 +++++++++++++++++- HttpSecurity.Example/Program.cs | 14 +-- 8 files changed, 194 insertions(+), 16 deletions(-) create mode 100644 HttpSecurity.AspNet/Enumerations/CrossOriginEmbedderPolicyDirective.cs create mode 100644 HttpSecurity.AspNet/Enumerations/CrossOriginOpenerPolicyDirective.cs create mode 100644 HttpSecurity.AspNet/Enumerations/CrossOriginResourcePolicyDirective.cs diff --git a/HttpSecurity.AspNet/ContentSecurityPolicies/BlockAllMixedContent.cs b/HttpSecurity.AspNet/ContentSecurityPolicies/BlockAllMixedContent.cs index 1419ece..4606a4b 100644 --- a/HttpSecurity.AspNet/ContentSecurityPolicies/BlockAllMixedContent.cs +++ b/HttpSecurity.AspNet/ContentSecurityPolicies/BlockAllMixedContent.cs @@ -2,8 +2,9 @@ /// -/// block-all-mixed-content policy - considered deprecated. +/// block-all-mixed-content policy - deprecated. Use upgrade-insecure-requests instead. /// +[Obsolete("block-all-mixed-content is deprecated by the CSP specification. Use upgrade-insecure-requests instead.")] [ContentSecurityPolicyOptions] public sealed partial class BlockAllMixedContentOptions : ContentSecurityPolicyOptionsBase { @@ -11,8 +12,9 @@ public sealed partial class BlockAllMixedContentOptions : ContentSecurityPolicyO /// -/// block-all-mixed-content policy - considered deprecated. +/// block-all-mixed-content policy - deprecated. Use upgrade-insecure-requests instead. /// +[Obsolete("block-all-mixed-content is deprecated by the CSP specification. Use upgrade-insecure-requests instead.")] [ContentSecurityPolicy("block-all-mixed-content")] public sealed partial class BlockAllMixedContent : ContentSecurityPolicyBase { diff --git a/HttpSecurity.AspNet/ContentSecurityPolicies/NavigateTo.cs b/HttpSecurity.AspNet/ContentSecurityPolicies/NavigateTo.cs index 3e9f634..fe1847a 100644 --- a/HttpSecurity.AspNet/ContentSecurityPolicies/NavigateTo.cs +++ b/HttpSecurity.AspNet/ContentSecurityPolicies/NavigateTo.cs @@ -2,8 +2,9 @@ /// -/// navigate-to policy. +/// navigate-to policy - removed from the CSP Level 3 specification with no browser support. /// +[Obsolete("navigate-to was removed from the CSP Level 3 specification and has no browser support. Remove this directive.")] [ContentSecurityPolicyOptions] [AddHashValue] [AddHostSource] @@ -23,8 +24,9 @@ public sealed partial class NavigateToOptions : ContentSecurityPolicyOptionsBase /// -/// navigate-to policy. +/// navigate-to policy - removed from the CSP Level 3 specification with no browser support. /// +[Obsolete("navigate-to was removed from the CSP Level 3 specification and has no browser support. Remove this directive.")] [ContentSecurityPolicy("navigate-to")] public sealed partial class NavigateTo : ContentSecurityPolicyBase { diff --git a/HttpSecurity.AspNet/Enumerations/CrossOriginEmbedderPolicyDirective.cs b/HttpSecurity.AspNet/Enumerations/CrossOriginEmbedderPolicyDirective.cs new file mode 100644 index 0000000..a67a31f --- /dev/null +++ b/HttpSecurity.AspNet/Enumerations/CrossOriginEmbedderPolicyDirective.cs @@ -0,0 +1,22 @@ +namespace HttpSecurity.AspNet; + +/// +/// Directives for the Cross-Origin-Embedder-Policy header. +/// +public enum CrossOriginEmbedderPolicyDirective +{ + /// + /// unsafe-none — allows the document to fetch cross-origin resources without giving explicit permission (default). + /// + UnsafeNone, + + /// + /// require-corp — prevents loading of cross-origin resources that don't explicitly grant permission via CORS or CORP. + /// + RequireCorp, + + /// + /// credentialless — allows cross-origin no-cors requests to be sent without credentials. + /// + Credentialless, +} diff --git a/HttpSecurity.AspNet/Enumerations/CrossOriginOpenerPolicyDirective.cs b/HttpSecurity.AspNet/Enumerations/CrossOriginOpenerPolicyDirective.cs new file mode 100644 index 0000000..e9498e2 --- /dev/null +++ b/HttpSecurity.AspNet/Enumerations/CrossOriginOpenerPolicyDirective.cs @@ -0,0 +1,22 @@ +namespace HttpSecurity.AspNet; + +/// +/// Directives for the Cross-Origin-Opener-Policy header. +/// +public enum CrossOriginOpenerPolicyDirective +{ + /// + /// unsafe-none — allows the document to be added to its opener's browsing context group (default). + /// + UnsafeNone, + + /// + /// same-origin-allow-popups — retains references to newly opened windows or tabs that either don't set COOP or opt out. + /// + SameOriginAllowPopups, + + /// + /// same-origin — isolates the browsing context group to same-origin documents only. + /// + SameOrigin, +} diff --git a/HttpSecurity.AspNet/Enumerations/CrossOriginResourcePolicyDirective.cs b/HttpSecurity.AspNet/Enumerations/CrossOriginResourcePolicyDirective.cs new file mode 100644 index 0000000..e3f48ba --- /dev/null +++ b/HttpSecurity.AspNet/Enumerations/CrossOriginResourcePolicyDirective.cs @@ -0,0 +1,22 @@ +namespace HttpSecurity.AspNet; + +/// +/// Directives for the Cross-Origin-Resource-Policy header. +/// +public enum CrossOriginResourcePolicyDirective +{ + /// + /// same-site — only requests from the same site can read the resource. + /// + SameSite, + + /// + /// same-origin — only requests from the same origin can read the resource. + /// + SameOrigin, + + /// + /// cross-origin — requests from any origin can read the resource. + /// + CrossOrigin, +} diff --git a/HttpSecurity.AspNet/Enumerations/SchemeSource.cs b/HttpSecurity.AspNet/Enumerations/SchemeSource.cs index 3a202eb..0e7fa80 100644 --- a/HttpSecurity.AspNet/Enumerations/SchemeSource.cs +++ b/HttpSecurity.AspNet/Enumerations/SchemeSource.cs @@ -39,4 +39,16 @@ public enum SchemeSource /// mediastream: source. /// Mediastream, + + + /// + /// ws: source (unencrypted WebSocket). + /// + Ws, + + + /// + /// wss: source (encrypted WebSocket). + /// + Wss, } diff --git a/HttpSecurity.AspNet/Services/HttpSecurityOptions.cs b/HttpSecurity.AspNet/Services/HttpSecurityOptions.cs index 09b88de..526bf3c 100644 --- a/HttpSecurity.AspNet/Services/HttpSecurityOptions.cs +++ b/HttpSecurity.AspNet/Services/HttpSecurityOptions.cs @@ -50,11 +50,11 @@ internal string GetContentSecurityPolicy(IHttpSecurityService httpSecurityServic public HttpSecurityOptions AddContentSecurityOptions(Action configureOptions) { ContentSecurityPolicyOptions options = new(); - + configureOptions(options); HeaderBuilders.Add(new( - "Content-Security-Policy", + "Content-Security-Policy", (IHttpSecurityService httpSecurityService, string nonceValue, string baseUri, string baseDomain) => string.Join(' ', options.Policies.Select(x => x.GetPolicyValue(httpSecurityService, nonceValue, baseUri, baseDomain)).OrderBy(x => x)))); return this; @@ -121,12 +121,108 @@ public HttpSecurityOptions AddPermissionsPolicy(string permissionsPolicy) /// - /// Adds an Strict-Transport-Security directive with the value supplied. + /// Adds a Strict-Transport-Security header. + /// + /// The max-age value in seconds. + /// Whether to include the includeSubDomains directive. + /// Whether to include the preload directive. Only set this if you intend to submit the domain to the HSTS preload list at https://hstspreload.org. + /// + public HttpSecurityOptions AddStrictTransportSecurity(ulong maxAgeExpireTime, bool includeSubDomains = false, bool preload = false) + { + HeaderBuilders.Add(new("Strict-Transport-Security", (_, _, _, _) => + $"max-age={maxAgeExpireTime}{(includeSubDomains ? "; includeSubDomains" : "")}{(preload ? "; preload" : "")}")); + return this; + } + + + /// + /// Adds a Content-Security-Policy-Report-Only header. Use this to test a new policy without enforcing it. + /// + /// Configures the content security policy. + /// + public HttpSecurityOptions AddContentSecurityPolicyReportOnly(Action configureOptions) + { + ContentSecurityPolicyOptions options = new(); + + configureOptions(options); + + HeaderBuilders.Add(new( + "Content-Security-Policy-Report-Only", + (IHttpSecurityService httpSecurityService, string nonceValue, string baseUri, string baseDomain) => string.Join(' ', options.Policies.Select(x => x.GetPolicyValue(httpSecurityService, nonceValue, baseUri, baseDomain)).OrderBy(x => x)))); + + return this; + } + + + /// + /// Adds a Cross-Origin-Embedder-Policy header. + /// + /// The COEP directive value. + /// + public HttpSecurityOptions AddCrossOriginEmbedderPolicy(CrossOriginEmbedderPolicyDirective directive) + { + var value = directive switch + { + CrossOriginEmbedderPolicyDirective.UnsafeNone => "unsafe-none", + CrossOriginEmbedderPolicyDirective.RequireCorp => "require-corp", + CrossOriginEmbedderPolicyDirective.Credentialless => "credentialless", + _ => throw new NotImplementedException(), + }; + + HeaderBuilders.Add(new("Cross-Origin-Embedder-Policy", (_, _, _, _) => value)); + return this; + } + + + /// + /// Adds a Cross-Origin-Opener-Policy header. + /// + /// The COOP directive value. + /// + public HttpSecurityOptions AddCrossOriginOpenerPolicy(CrossOriginOpenerPolicyDirective directive) + { + var value = directive switch + { + CrossOriginOpenerPolicyDirective.UnsafeNone => "unsafe-none", + CrossOriginOpenerPolicyDirective.SameOriginAllowPopups => "same-origin-allow-popups", + CrossOriginOpenerPolicyDirective.SameOrigin => "same-origin", + _ => throw new NotImplementedException(), + }; + + HeaderBuilders.Add(new("Cross-Origin-Opener-Policy", (_, _, _, _) => value)); + return this; + } + + + /// + /// Adds a Cross-Origin-Resource-Policy header. + /// + /// The CORP directive value. + /// + public HttpSecurityOptions AddCrossOriginResourcePolicy(CrossOriginResourcePolicyDirective directive) + { + var value = directive switch + { + CrossOriginResourcePolicyDirective.SameSite => "same-site", + CrossOriginResourcePolicyDirective.SameOrigin => "same-origin", + CrossOriginResourcePolicyDirective.CrossOrigin => "cross-origin", + _ => throw new NotImplementedException(), + }; + + HeaderBuilders.Add(new("Cross-Origin-Resource-Policy", (_, _, _, _) => value)); + return this; + } + + + /// + /// Adds a Reporting-Endpoints header, required for the report-to CSP directive to function. /// + /// One or more named endpoint definitions, e.g. ("csp-endpoint", "https://example.com/csp-reports"). /// - public HttpSecurityOptions AddStrictTransportSecurity(ulong maxAgeExpireTime, bool includeSubDomains = false) + public HttpSecurityOptions AddReportingEndpoints(params (string name, string url)[] endpoints) { - HeaderBuilders.Add(new("Strict-Transport-Security", (_, _, _, _) => $"max-age={maxAgeExpireTime}{(includeSubDomains ? " includeSubDomains" : "")}")); + var value = string.Join(", ", endpoints.Select(e => $"{e.name}=\"{e.url}\"")); + HeaderBuilders.Add(new("Reporting-Endpoints", (_, _, _, _) => value)); return this; } diff --git a/HttpSecurity.Example/Program.cs b/HttpSecurity.Example/Program.cs index 206e9e0..0a479dd 100644 --- a/HttpSecurity.Example/Program.cs +++ b/HttpSecurity.Example/Program.cs @@ -16,8 +16,6 @@ cspOptions .AddBaseUri(o => o.AddSelf()) - .AddBlockAllMixedContent() - .AddChildSrc(o => o.AddSelf()) .AddConnectSrc(o => o @@ -54,34 +52,36 @@ .AddReportUri(o => o.AddUri((baseUri, baseDomain) => $"https://{baseUri}/api/CspReporting/UriReport")) .AddScriptSrc(o => o - .AddSelf() .AddNonce() .AddHashValue(HashAlgorithm.SHA256, "v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA=") - // StrictDynamic works on Chromium browsers but fails for both Firefox and Safari - //.AddStrictDynamicIf(() => !builder.Environment.IsDevelopment()) + .AddStrictDynamic() .AddReportSample() .AddUri("https://www.googletagmanager.com/gtag/js") .AddUri((baseUri, baseDomain) => $"https://{baseUri}/_framework/aspnetcore-browser-refresh.js") .AddUri((baseUri, baseDomain) => $"https://{baseUri}/_framework/blazor.server.js") .AddGeneratedHashValues(StaticFileExtension.JS)) + .AddScriptSrcAttr(o => o.AddNone()) + .AddStyleSrc(o => o .AddSelf() .AddUnsafeInline() - .AddUnsafeHashes() .AddReportSample()) .AddUpgradeInsecureRequests() .AddWorkerSrc(o => o.AddSelf()); }) + .AddCrossOriginOpenerPolicy(CrossOriginOpenerPolicyDirective.SameOrigin) + .AddCrossOriginEmbedderPolicy(CrossOriginEmbedderPolicyDirective.RequireCorp) + .AddCrossOriginResourcePolicy(CrossOriginResourcePolicyDirective.SameOrigin) .AddReferrerPolicy(ReferrerPolicyDirective.NoReferrer) .AddPermissionsPolicy("accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()") .AddStrictTransportSecurity(31536000, true) .AddXClientId("HttpSecurity.Example") .AddXContentTypeOptionsNoSniff() .AddXFrameOptionsDirective(XFrameOptionsDirective.Deny) - .AddXXssProtectionDirective(XXssProtectionDirective.OneModeBlock) + .AddXXssProtectionDirective(XXssProtectionDirective.Zero) .AddXPermittedCrossDomainPoliciesDirective(XPermittedCrossDomainPoliciesDirective.None); }, onStartingOptions =>