Skip to content

Commit f4b5b73

Browse files
author
MPCoreDeveloper
committed
modernization of code base , cleanup , upgrade tests to XunitV3
1 parent 534c4a2 commit f4b5b73

22 files changed

+278
-134
lines changed

CHANGELOG.md

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,64 @@
11
# Changelog
22

3-
All notable changes to this project will be documented in this file.
3+
All notable changes to SafeWebCore will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
---
9+
810
## [Unreleased]
911

12+
## [1.1.0] — 2025-06-28
13+
14+
### Added
15+
16+
- **`HttpContext.GetCspNonce()` extension method** — Discoverable way to retrieve the per-request CSP nonce without magic strings. Available via `using SafeWebCore.Extensions;`.
17+
```csharp
18+
var nonce = HttpContext.GetCspNonce();
19+
```
20+
- **`NonceService.TryWriteNonce(Span<char>, out int)`** — Zero-allocation overload that writes the nonce directly into a caller-provided buffer. Ideal for high-throughput scenarios or writing directly into response buffers.
21+
```csharp
22+
Span<char> buffer = stackalloc char[NonceService.NonceLength];
23+
if (nonceService.TryWriteNonce(buffer, out int written))
24+
{
25+
// Use buffer[..written] — no heap allocation
26+
}
27+
```
28+
- **`NonceService.NonceLength` constant** — Public constant (44) for the length of a generated nonce string. Eliminates magic numbers when pre-allocating buffers.
29+
30+
### Changed
31+
32+
- **CSP template is now pre-built once** in the middleware constructor instead of being rebuilt on every request. Only the lightweight `string.Replace("{nonce}", nonce)` runs per-request. This significantly reduces per-request allocations.
33+
- **`CspOptions.Build()` uses `StringBuilder`** — Replaced `List<string>` + interpolated string allocations + `string.Join` with a pre-sized `StringBuilder(512)`. Eliminates ~20 intermediate string allocations per call.
34+
- **`CspReportMiddleware` now passes `CancellationToken`**`ReadToEndAsync` uses `context.RequestAborted` for proper cancellation when clients disconnect.
35+
- **`CspNonceAttribute` uses C# pattern matching** — Collapsed nested conditionals into a single `is string { Length: > 0 } nonce` pattern expression.
36+
- **Preset application extracted to `ApplyPreset` helper** — Internal `NetSecureHeadersOptions.ApplyPreset()` method consolidates the 20+ line property copy into a single reusable call. Adding new options in the future requires updating only one place.
37+
38+
### Compatibility
39+
40+
-**100% backwards compatible** with v1.0.0
41+
- All existing public APIs (`AddNetSecureHeadersStrictAPlus`, `UseNetSecureHeaders`, `CspBuilder`, `[CspNonce]`, CSP reporting) remain unchanged
42+
- No breaking changes to method signatures, behavior, or configuration
43+
- All 40 existing tests pass without modification
44+
45+
---
46+
47+
## [1.0.0] — 2025-06-15
48+
1049
### Added
1150

12-
- Initial project structure
13-
- CSP middleware foundation
14-
- xUnit test project
51+
- Strict A+ preset — `AddNetSecureHeadersStrictAPlus()` for one-line A+ configuration on securityheaders.com
52+
- Fluent `CspBuilder` with full CSP Level 3 (W3C Recommendation) directive coverage
53+
- CSP Level 4 support — Trusted Types (`require-trusted-types-for`, `trusted-types`), `fenced-frame-src`
54+
- Per-request cryptographic nonce generation with `stackalloc` + `RandomNumberGenerator` (zero heap allocations)
55+
- `[CspNonce]` action filter attribute for Razor view nonce injection
56+
- Built-in CSP violation reporting middleware (`/csp-report` endpoint)
57+
- Extensible `IHeaderPolicy` interface for custom header policies
58+
- Full security header suite: HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, COEP, COOP, CORP, X-DNS-Prefetch-Control, X-Permitted-Cross-Domain-Policies
59+
- Server header removal
60+
- Comprehensive documentation and test suite
61+
62+
[Unreleased]: https://github.com/MPCoreDeveloper/SafeWebCore/compare/v1.1.0...HEAD
63+
[1.1.0]: https://github.com/MPCoreDeveloper/SafeWebCore/compare/v1.0.0...v1.1.0
64+
[1.0.0]: https://github.com/MPCoreDeveloper/SafeWebCore/releases/tag/v1.0.0

PACKAGE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ Both methods are defined in **`SafeWebCore.Extensions.ServiceCollectionExtension
130130
- 📋 **Full CSP Level 3** (W3C Recommendation) — all 22 directives, nonce/hash support, `strict-dynamic`, `report-to`, `worker-src`, `frame-src`, `manifest-src`, `script-src-elem/attr`, `style-src-elem/attr`
131131
- 🔮 **CSP Level 4 ready** — Trusted Types (`require-trusted-types-for`, `trusted-types`), `fenced-frame-src` (Privacy Sandbox)
132132
- 🎯 **Fluent CSP Builder** — type-safe, chainable API with full XML documentation
133-
-**Zero-allocation nonce generation**`stackalloc` + `RandomNumberGenerator`
133+
-**Zero-allocation nonce generation**`stackalloc` + `RandomNumberGenerator`, plus `TryWriteNonce(Span<char>)` for fully heap-free scenarios *(v1.1.0)*
134+
- 🔍 **`HttpContext.GetCspNonce()`** — discoverable extension method to retrieve the per-request nonce *(v1.1.0)*
135+
- 🚀 **Pre-built CSP template** — CSP header string computed once at startup, not per-request *(v1.1.0)*
134136
- 🔌 **Extensible** — custom `IHeaderPolicy` implementations
135137
- 📊 **CSP violation reporting** — built-in `/csp-report` endpoint using Reporting API v1
136138

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
- 📋 **Full CSP Level 3** (W3C Recommendation) — all directives including `worker-src`, `manifest-src`, `frame-src`, `script-src-elem/attr`, `style-src-elem/attr`, `report-to`, nonce/hash support, `strict-dynamic`
2121
- 🔮 **CSP Level 4 ready** — Trusted Types (`require-trusted-types-for`, `trusted-types`), `fenced-frame-src` (Privacy Sandbox)
2222
- 🎯 **Fluent CSP Builder** — type-safe, chainable API with full XML documentation for every directive
23-
-**Zero-allocation nonce generation**`stackalloc` + `RandomNumberGenerator` on the hot path
23+
-**Zero-allocation nonce generation**`stackalloc` + `RandomNumberGenerator` on the hot path, plus `TryWriteNonce(Span<char>)` for fully heap-free scenarios
24+
- 🔍 **`HttpContext.GetCspNonce()`** — discoverable extension method to retrieve the per-request nonce
2425
- 🛑 **Server header removal** — hides server technology from attackers
2526
- 🔌 **Extensible** — add custom `IHeaderPolicy` implementations for any header
2627
- 📊 **CSP violation reporting** — built-in middleware for `/csp-report` endpoint using Reporting API v1
@@ -31,7 +32,24 @@
3132
|----------|--------|----------|
3233
| **CSP Level 3** (W3C Recommendation) | ✅ Full | All 22 directives, nonce/hash, `strict-dynamic`, `report-to` |
3334
| **CSP Level 4** (Emerging) | ✅ Ready | Trusted Types, `fenced-frame-src` (Privacy Sandbox) |
34-
| Deprecated directives | ✅ Handled | `report-uri`, `block-all-mixed-content` marked `[Obsolete]` |
35+
36+
---
37+
38+
## 🆕 What's New in v1.1.0
39+
40+
v1.1.0 is a **performance and developer-experience** release — fully backwards compatible with v1.0.0.
41+
42+
| Improvement | Detail |
43+
|-------------|--------|
44+
| **Pre-built CSP template** | CSP header string is computed once at startup, not per-request |
45+
| **`StringBuilder`-based `Build()`** | Eliminates ~20 intermediate string allocations in CSP header generation |
46+
| **`HttpContext.GetCspNonce()`** | New extension method — no more magic string lookups |
47+
| **`NonceService.TryWriteNonce(Span<char>)`** | Zero-allocation nonce generation for high-throughput paths |
48+
| **`NonceService.NonceLength`** | Public constant (44) for pre-allocating nonce buffers |
49+
| **CancellationToken in CSP reporting** | Report reads now respect client disconnects |
50+
| **Modern C# patterns** | Pattern matching, cleaner preset application, reduced boilerplate |
51+
52+
See the full [CHANGELOG](CHANGELOG.md) for details.
3553

3654
---
3755

@@ -166,6 +184,15 @@ public class HomeController : Controller
166184
</style>
167185
```
168186

187+
### Direct access via `GetCspNonce()` extension *(v1.1.0+)*
188+
189+
```csharp
190+
using SafeWebCore.Extensions;
191+
192+
// In Minimal APIs, middleware, Razor Pages, etc.
193+
var nonce = HttpContext.GetCspNonce();
194+
```
195+
169196
### Direct access via `HttpContext.Items`
170197

171198
```csharp

docs/advanced-configuration.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,11 @@ app.Run();
151151
### In Minimal APIs
152152

153153
```csharp
154+
using SafeWebCore.Extensions;
155+
154156
app.MapGet("/", (HttpContext ctx) =>
155157
{
156-
var nonce = ctx.Items[NetSecureHeaders.CspNonceKey] as string;
158+
var nonce = ctx.GetCspNonce();
157159
return Results.Content($"""
158160
<html>
159161
<body>
@@ -183,13 +185,15 @@ public class DashboardController : Controller
183185
### In Razor Pages
184186

185187
```csharp
188+
using SafeWebCore.Extensions;
189+
186190
public class IndexModel : PageModel
187191
{
188192
public string? CspNonce { get; private set; }
189193

190194
public void OnGet()
191195
{
192-
CspNonce = HttpContext.Items[NetSecureHeaders.CspNonceKey] as string;
196+
CspNonce = HttpContext.GetCspNonce();
193197
}
194198
}
195199
```
@@ -273,7 +277,7 @@ public class SecurityHeadersTests : IAsyncDisposable
273277

274278
| Problem | Cause | Fix |
275279
|---------|-------|-----|
276-
| Inline scripts blocked | Missing nonce attribute | Add `nonce="@ViewData["CspNonce"]"` to `<script>` tags |
280+
| Inline scripts blocked | Missing nonce attribute | Add `nonce="@ViewData["CspNonce"]"` to `<script>` tags, or use `HttpContext.GetCspNonce()` |
277281
| Styles not loading | Missing nonce on `<style>` | Add nonce to `<style>` and `<link>` elements |
278282
| Google Fonts blocked | `font-src` too restrictive | Add `https://fonts.gstatic.com` to `font-src` |
279283
| API calls failing | `connect-src` doesn't include API origin | Add your API URL to `connect-src` |

docs/csp-configuration.md

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ These control where resources can be loaded from.
7575
| Directive | Purpose | Strict A+ Value |
7676
|-----------|---------|-----------------|
7777
| `upgrade-insecure-requests` | Auto-upgrade HTTP → HTTPS | ✅ Enabled |
78-
| `block-all-mixed-content` | Block mixed content _(deprecated)_ | ❌ Disabled |
7978

8079
---
8180

@@ -198,15 +197,38 @@ public class HomeController : Controller
198197

199198
### Direct access from HttpContext
200199

200+
The recommended way to get the nonce is via the `GetCspNonce()` extension method *(v1.1.0+)*:
201+
201202
```csharp
202-
// In middleware, minimal API handlers, etc.
203+
using SafeWebCore.Extensions;
204+
205+
// In middleware, minimal API handlers, Razor Pages, etc.
203206
app.MapGet("/api/nonce", (HttpContext ctx) =>
204207
{
205-
var nonce = ctx.Items[NetSecureHeaders.CspNonceKey] as string;
208+
var nonce = ctx.GetCspNonce();
206209
return Results.Ok(new { nonce });
207210
});
208211
```
209212

213+
Or use `HttpContext.Items` directly:
214+
215+
```csharp
216+
var nonce = ctx.Items[NetSecureHeaders.CspNonceKey] as string;
217+
```
218+
219+
### Zero-allocation nonce access *(v1.1.0+)*
220+
221+
For high-throughput scenarios, `NonceService.TryWriteNonce` writes the nonce directly into a `Span<char>` with zero heap allocation:
222+
223+
```csharp
224+
Span<char> buffer = stackalloc char[NonceService.NonceLength];
225+
if (nonceService.TryWriteNonce(buffer, out int written))
226+
{
227+
ReadOnlySpan<char> nonce = buffer[..written];
228+
// Use nonce directly — no string allocation
229+
}
230+
```
231+
210232
---
211233

212234
## The `{nonce}` Placeholder
@@ -308,15 +330,14 @@ SafeWebCore implements the **complete CSP Level 3** W3C Recommendation and inclu
308330
| **Nonce-based** | `'nonce-{nonce}'` per-request cryptographic nonces ||
309331
| **Hash-based** | `'sha256-...'`, `'sha384-...'`, `'sha512-...'` allowlisting ||
310332
| **Trust propagation** | `'strict-dynamic'` — trusted scripts can load further dependencies ||
311-
| **Deprecated (L3)** | `report-uri``[Obsolete]`, `block-all-mixed-content``[Obsolete]` | ✅ Handled |
312333

313334
#### Key CSP Level 3 improvements implemented
314335

315336
- **`frame-src` split from `child-src`** — In CSP Level 2, `child-src` governed both frames and workers. Level 3 separates them: `frame-src` for `<frame>`/`<iframe>`, `worker-src` for Worker/SharedWorker/ServiceWorker.
316337
- **`worker-src`** — Dedicated directive for controlling Worker, SharedWorker, and ServiceWorker sources.
317338
- **`manifest-src`** — Controls web app manifest loading.
318339
- **Granular script/style directives**`script-src-elem`, `script-src-attr`, `style-src-elem`, `style-src-attr` provide fine-grained control beyond the base `script-src`/`style-src`.
319-
- **`report-to`**Replaces the deprecated `report-uri` directive with the modern Reporting API v1.
340+
- **`report-to`**Modern Reporting API v1 for CSP violation reporting.
320341
- **Nonce + hash + `strict-dynamic`** — The recommended approach per Google and the W3C. SafeWebCore generates a unique cryptographic nonce per request using `stackalloc` + `RandomNumberGenerator` (zero heap allocations).
321342

322343
### CSP Level 4 (Emerging) — ✅ Ready
@@ -355,17 +376,6 @@ opts.Csp = new CspBuilder()
355376

356377
Supported algorithms: `sha256`, `sha384`, `sha512`.
357378

358-
### Deprecated Directives
359-
360-
SafeWebCore correctly handles deprecated directives:
361-
362-
| Directive | Status | Replacement |
363-
|-----------|--------|-------------|
364-
| `report-uri` | `[Obsolete]` — still emitted when set for backward compatibility | `report-to` (Reporting API v1) |
365-
| `block-all-mixed-content` | `[Obsolete]` — modern browsers block mixed content by default | `upgrade-insecure-requests` |
366-
367-
Both deprecated directives are excluded from `CspBuilder` but remain available on `CspOptions` with `[Obsolete]` attributes and compiler warnings.
368-
369379
---
370380

371381
## Validate Your CSP

docs/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dotnet add package SafeWebCore
1414
Or add to your `.csproj`:
1515

1616
```xml
17-
<PackageReference Include="SafeWebCore" Version="1.0.0" />
17+
<PackageReference Include="SafeWebCore" Version="1.1.0" />
1818
```
1919

2020
## Minimal Setup (A+ in 3 lines)

docs/presets.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ This is useful for:
209209
- Building custom presets based on the strict A+ baseline
210210
- Inspecting the exact values at startup
211211

212+
### Building custom presets *(v1.1.0+)*
213+
214+
Internally, `AddNetSecureHeadersStrictAPlus` uses an `ApplyPreset` helper to copy all preset values. You can inspect `SecurePresets.StrictAPlus()` as a baseline and override properties using the customize callback — without needing to create a full custom configuration.
215+
212216
---
213217

214218
## When to NOT Use Strict A+

src/SafeWebCore/Attributes/CspNonceAttribute.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,10 @@ public override void OnActionExecuted(ActionExecutedContext context)
1717
{
1818
ArgumentNullException.ThrowIfNull(context);
1919

20-
if (context.Result is ViewResult viewResult)
20+
if (context.Result is ViewResult viewResult
21+
&& context.HttpContext.Items[NetSecureHeaders.CspNonceKey] is string { Length: > 0 } nonce)
2122
{
22-
var nonce = context.HttpContext.Items[NetSecureHeaders.CspNonceKey] as string;
23-
if (!string.IsNullOrEmpty(nonce))
24-
{
25-
viewResult.ViewData["CspNonce"] = nonce;
26-
}
23+
viewResult.ViewData["CspNonce"] = nonce;
2724
}
2825

2926
base.OnActionExecuted(context);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Microsoft.AspNetCore.Http;
2+
3+
namespace SafeWebCore.Extensions;
4+
5+
/// <summary>
6+
/// Extension methods for accessing SafeWebCore features from <see cref="HttpContext"/>.
7+
/// </summary>
8+
public static class HttpContextExtensions
9+
{
10+
/// <summary>
11+
/// Gets the CSP nonce generated for the current request by the security headers middleware.
12+
/// Returns <see langword="null"/> if the middleware has not run or CSP is disabled.
13+
/// </summary>
14+
/// <param name="context">The HTTP context for the current request.</param>
15+
/// <returns>The base64-encoded nonce string, or <see langword="null"/> if unavailable.</returns>
16+
/// <example>
17+
/// <code>
18+
/// var nonce = HttpContext.GetCspNonce();
19+
/// &lt;script nonce="@nonce"&gt;console.log('safe');&lt;/script&gt;
20+
/// </code>
21+
/// </example>
22+
public static string? GetCspNonce(this HttpContext context)
23+
{
24+
ArgumentNullException.ThrowIfNull(context);
25+
26+
return context.Items[NetSecureHeaders.CspNonceKey] as string;
27+
}
28+
}

src/SafeWebCore/Extensions/ServiceCollectionExtensions.cs

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -50,33 +50,7 @@ public static IServiceCollection AddNetSecureHeadersStrictAPlus(
5050

5151
return AddNetSecureHeaders(services, opts =>
5252
{
53-
var preset = SecurePresets.StrictAPlus();
54-
55-
// Copy all preset values into the options instance
56-
opts.EnableHsts = preset.EnableHsts;
57-
opts.HstsValue = preset.HstsValue;
58-
opts.EnableXFrameOptions = preset.EnableXFrameOptions;
59-
opts.XFrameOptionsValue = preset.XFrameOptionsValue;
60-
opts.EnableXContentTypeOptions = preset.EnableXContentTypeOptions;
61-
opts.XContentTypeOptionsValue = preset.XContentTypeOptionsValue;
62-
opts.EnableReferrerPolicy = preset.EnableReferrerPolicy;
63-
opts.ReferrerPolicyValue = preset.ReferrerPolicyValue;
64-
opts.EnablePermissionsPolicy = preset.EnablePermissionsPolicy;
65-
opts.PermissionsPolicyValue = preset.PermissionsPolicyValue;
66-
opts.EnableCoep = preset.EnableCoep;
67-
opts.CoepValue = preset.CoepValue;
68-
opts.EnableCoop = preset.EnableCoop;
69-
opts.CoopValue = preset.CoopValue;
70-
opts.EnableCorp = preset.EnableCorp;
71-
opts.CorpValue = preset.CorpValue;
72-
opts.EnableXDnsPrefetchControl = preset.EnableXDnsPrefetchControl;
73-
opts.XDnsPrefetchControlValue = preset.XDnsPrefetchControlValue;
74-
opts.EnableXPermittedCrossDomainPolicies = preset.EnableXPermittedCrossDomainPolicies;
75-
opts.XPermittedCrossDomainPoliciesValue = preset.XPermittedCrossDomainPoliciesValue;
76-
opts.RemoveServerHeader = preset.RemoveServerHeader;
77-
opts.EnableCsp = preset.EnableCsp;
78-
opts.Csp = preset.Csp;
79-
opts.CustomPolicies = preset.CustomPolicies;
53+
opts.ApplyPreset(SecurePresets.StrictAPlus());
8054

8155
// Allow the caller to override specific values
8256
customize?.Invoke(opts);

0 commit comments

Comments
 (0)