Skip to content

Commit aecc977

Browse files
committed
Enhance security features in ServerSync and TabSync
- Added security requirements and guidelines in IStoreHub.cs for authorization, document access validation, rate limiting, and message size limits. - Implemented message signing, rate limiting, and message size validation in ServerSyncMiddleware.cs. - Introduced ServerSyncOptions.cs properties for validation requirements, message size limits, rate limiting, and message signing. - Created ServerSyncSecurityExtensions.cs for configuring secure defaults and validation callbacks. - Updated StateUpdate.cs to include HMAC-SHA256 signature for message integrity verification. - Enhanced TabSync with message signing capabilities and key derivation options in TabSyncOptions.cs. - Added JavaScript function to derive signing key from window location for cross-tab synchronization. - Updated test projects to target .NET 10 and updated dependencies for compatibility. - Adjusted persistence tests to validate state serialization correctly.
1 parent 61e81f3 commit aecc977

35 files changed

Lines changed: 2275 additions & 65 deletions

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,18 @@ public class MyPlugin : StorePluginBase<AppState>
651651

652652
## Security
653653

654+
**IMPORTANT**: Review [`docs/SECURITY.md`](docs/SECURITY.md) before deploying to production.
655+
656+
### Critical Security Requirements
657+
658+
Before production deployment:
659+
660+
1. **Disable DevTools** - Use `#if DEBUG` to remove `.WithDevTools()`
661+
2. **Mark Sensitive Data** - Add `[SensitiveData]` to passwords, tokens, keys
662+
3. **Validate External State** - Implement `IStateValidator<T>` for persistence/sync
663+
4. **Never Persist Secrets** - Use `TransformOnSave` to exclude sensitive fields
664+
5. **Enable Message Signing** - Call `.EnableMessageSigning()` for TabSync
665+
654666
### Sensitive Data Filtering
655667

656668
Prevent passwords/tokens from appearing in DevTools:
@@ -665,6 +677,16 @@ public record UserState(
665677
// In DevTools: { Name: "John", Password: "[REDACTED]", ApiToken: "[REDACTED]" }
666678
```
667679

680+
**WARNING**: DevTools should NEVER be shipped to production. Always use:
681+
682+
```csharp
683+
#if DEBUG
684+
.WithDefaults(sp, "MyStore") // DevTools + Logging
685+
#else
686+
.WithLogging() // Logging only
687+
#endif
688+
```
689+
668690
### State Validation
669691

670692
```csharp

samples/EasyAppDev.Blazor.Store.Sample/EasyAppDev.Blazor.Store.Sample.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.15" />
11-
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.15" PrivateAssets="all" />
10+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0-preview.1.25120.3" />
11+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0-preview.1.25120.3" PrivateAssets="all" />
1212
</ItemGroup>
1313

1414
<ItemGroup>

samples/EasyAppDev.Blazor.Store.Sample/Program.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323

2424
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
2525

26-
// Register diagnostics service - only available in DEBUG builds
26+
// ============================================================================
27+
// SECURITY NOTE: Diagnostics are DEBUG-only and expose full state snapshots
28+
// Never ship diagnostic features to production
29+
// ============================================================================
2730
#if DEBUG
2831
builder.Services.AddStoreDiagnostics();
2932
#endif
@@ -46,6 +49,8 @@
4649

4750
// ============================================================================
4851
// Counter Store - Basic example with DevTools and Logging
52+
// SECURITY: WithDefaults() includes DevTools - only use in DEBUG builds
53+
// For production, use .WithLogging() instead
4954
// ============================================================================
5055
builder.Services.AddStore(
5156
new CounterState(0),
@@ -92,6 +97,8 @@
9297
// Shopping Cart Store - Demonstrates persistence with LocalStorage
9398
// State survives page refreshes and browser restarts
9499
// WithPersistence automatically loads and saves state - no manual hydration needed!
100+
// SECURITY: For production with sensitive data, use TransformOnSave to exclude
101+
// sensitive fields and add IStateValidator to validate hydrated state
95102
// ============================================================================
96103
builder.Services.AddStore(
97104
ShoppingCartState.Empty,
@@ -185,6 +192,11 @@
185192
// ============================================================================
186193
// Tab Sync Demo Store - Demonstrates cross-tab synchronization
187194
// Real-time state sync across browser tabs using BroadcastChannel
195+
// SECURITY: For production with sensitive data, enable message signing:
196+
// .EnableMessageSigning()
197+
// .RequireValidSignature(true)
198+
// .MaxMessageAgeSeconds(30)
199+
// .ValidateTimestamp(true)
188200
// ============================================================================
189201
builder.Services.AddStore(
190202
TabSyncDemoState.Initial,
@@ -207,6 +219,9 @@
207219
// ============================================================================
208220
// Security Demo Store - Demonstrates sensitive data filtering
209221
// Properties marked with [SensitiveData] are filtered from DevTools
222+
// SECURITY BEST PRACTICE: Always mark passwords, tokens, API keys, and PII
223+
// with [SensitiveData] attribute to prevent exposure in DevTools/logs
224+
// Example: [property: SensitiveData] string Password
210225
// ============================================================================
211226
builder.Services.AddStore(
212227
SecurityDemoState.Initial,

samples/EasyAppDev.Blazor.Store.ServerSample/EasyAppDev.Blazor.Store.ServerSample.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
77
</PropertyGroup>

samples/EasyAppDev.Blazor.Store.ServerSample/Program.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
// ============================================================================
1515
// SINGLETON STORE - Shared across ALL users (demonstrates the problem)
16+
// SECURITY: Singleton stores should NOT contain user-specific data
17+
// Use scoped stores for per-user isolation
1618
// ============================================================================
1719
builder.Services.AddStore(
1820
new SingletonCounterState(),
@@ -21,6 +23,8 @@
2123
// ============================================================================
2224
// SCOPED STORES - Isolated per user/circuit (the solution!)
2325
// With IServiceProvider access, DevTools now work!
26+
// SECURITY: Scoped stores provide per-user isolation - use for user-specific data
27+
// For production, wrap DevTools with #if DEBUG
2428
// ============================================================================
2529

2630
// Scoped counter - each user gets their own

src/EasyAppDev.Blazor.Store/Blazor/StoreBuilderExtensions.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ namespace EasyAppDev.Blazor.Store.Blazor;
1414
public static class StoreBuilderExtensions
1515
{
1616
/// <summary>
17-
/// Applies default middleware configuration: DevTools and Logging.
17+
/// Applies default middleware configuration: DevTools (DEBUG only) and Logging.
1818
/// Works in all render modes (Server, WebAssembly, Auto) with lazy IJSRuntime resolution.
19+
/// DevTools are automatically disabled in Release builds for security.
1920
/// </summary>
2021
/// <typeparam name="TState">The type of state.</typeparam>
2122
/// <param name="builder">The store builder.</param>
@@ -28,10 +29,33 @@ public static StoreBuilder<TState> WithDefaults<TState>(
2829
string? storeName = null)
2930
where TState : notnull
3031
{
31-
// Use service provider-based DevTools for lazy IJSRuntime resolution
32-
// Works in Server (gracefully fails), WASM (succeeds), and Auto (Server→WASM transition)
33-
return builder.WithDevTools(serviceProvider, storeName ?? typeof(TState).Name)
34-
.WithLogging();
32+
return builder.WithDefaults(serviceProvider, storeName, includeDevTools: true);
33+
}
34+
35+
/// <summary>
36+
/// Applies default middleware configuration with explicit DevTools control.
37+
/// DevTools are only active in DEBUG builds and can be further controlled via the includeDevTools parameter.
38+
/// </summary>
39+
/// <typeparam name="TState">The type of state.</typeparam>
40+
/// <param name="builder">The store builder.</param>
41+
/// <param name="serviceProvider">The service provider for resolving dependencies.</param>
42+
/// <param name="storeName">The name to display in DevTools. Defaults to the state type name.</param>
43+
/// <param name="includeDevTools">Whether to include DevTools (only works in DEBUG builds).</param>
44+
/// <returns>The configured builder for chaining.</returns>
45+
public static StoreBuilder<TState> WithDefaults<TState>(
46+
this StoreBuilder<TState> builder,
47+
IServiceProvider serviceProvider,
48+
string? storeName,
49+
bool includeDevTools)
50+
where TState : notnull
51+
{
52+
#if DEBUG
53+
if (includeDevTools)
54+
{
55+
builder = builder.WithDevTools(serviceProvider, storeName ?? typeof(TState).Name);
56+
}
57+
#endif
58+
return builder.WithLogging();
3559
}
3660

3761
/// <summary>

src/EasyAppDev.Blazor.Store/Core/StoreBuilder.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,19 +188,26 @@ public StoreBuilder<TState> OnError(Action<StoreError<TState>> handler)
188188
/// <summary>
189189
/// Enables Redux DevTools integration with lazy IJSRuntime resolution.
190190
/// Works in all render modes: Server, WebAssembly, and Auto (Server → WASM).
191+
/// WARNING: DevTools are only available in DEBUG builds. In Release builds, this method is a no-op.
192+
/// DevTools expose your application state and should never be used in production.
191193
/// </summary>
192194
/// <param name="serviceProvider">Service provider to resolve IJSRuntime on-demand.</param>
193195
/// <param name="storeName">The name to display in DevTools. Defaults to the state type name.</param>
194196
/// <returns>The builder instance for chaining.</returns>
195197
public StoreBuilder<TState> WithDevTools(IServiceProvider serviceProvider, string? storeName = null)
196198
{
199+
#if DEBUG
197200
ArgumentNullException.ThrowIfNull(serviceProvider);
198201

199202
var devToolsMiddleware = new DevToolsMiddleware<TState>(
200203
serviceProvider,
201204
storeName ?? typeof(TState).Name);
202205

203206
return WithMiddleware(devToolsMiddleware);
207+
#else
208+
// No-op in Release builds for security
209+
return this;
210+
#endif
204211
}
205212

206213
/// <summary>

src/EasyAppDev.Blazor.Store/DevTools/DevToolsBuilderExtensions.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ namespace EasyAppDev.Blazor.Store.DevTools;
1212
/// </summary>
1313
public static class DevToolsBuilderExtensions
1414
{
15+
#if DEBUG
1516
/// <summary>
1617
/// Enables enhanced Redux DevTools integration with full time-travel support.
18+
/// WARNING: DevTools are only available in DEBUG builds. In Release builds, this method is a no-op.
19+
/// DevTools expose your application state and should never be used in production.
1720
/// </summary>
1821
/// <typeparam name="TState">The type of state.</typeparam>
1922
/// <param name="builder">The store builder.</param>
@@ -25,7 +28,7 @@ public static class DevToolsBuilderExtensions
2528
/// builder.WithEnhancedDevTools(sp, options =>
2629
/// {
2730
/// options.Name = "MyStore";
28-
/// options.EnableStateEditing = true;
31+
/// options.EnableStateEditing = false; // Disabled by default for security
2932
/// options.MaxHistory = 50;
3033
/// options.StateSanitizer = state => state with { Password = "***" };
3134
/// });
@@ -51,6 +54,8 @@ public static StoreBuilder<TState> WithEnhancedDevTools<TState>(
5154

5255
/// <summary>
5356
/// Enables enhanced Redux DevTools with default configuration.
57+
/// WARNING: DevTools are only available in DEBUG builds. In Release builds, this method is a no-op.
58+
/// DevTools expose your application state and should never be used in production.
5459
/// </summary>
5560
/// <typeparam name="TState">The type of state.</typeparam>
5661
/// <param name="builder">The store builder.</param>
@@ -68,4 +73,33 @@ public static StoreBuilder<TState> WithEnhancedDevTools<TState>(
6873
options.Name = storeName;
6974
});
7075
}
76+
#else
77+
/// <summary>
78+
/// DevTools stub for Release builds. This method does nothing in production.
79+
/// DevTools are disabled in Release builds for security reasons.
80+
/// </summary>
81+
public static StoreBuilder<TState> WithEnhancedDevTools<TState>(
82+
this StoreBuilder<TState> builder,
83+
IServiceProvider serviceProvider,
84+
Action<DevToolsOptions<TState>>? configure = null)
85+
where TState : notnull
86+
{
87+
// No-op in Release builds
88+
return builder;
89+
}
90+
91+
/// <summary>
92+
/// DevTools stub for Release builds. This method does nothing in production.
93+
/// DevTools are disabled in Release builds for security reasons.
94+
/// </summary>
95+
public static StoreBuilder<TState> WithEnhancedDevTools<TState>(
96+
this StoreBuilder<TState> builder,
97+
IServiceProvider serviceProvider,
98+
string storeName)
99+
where TState : notnull
100+
{
101+
// No-op in Release builds
102+
return builder;
103+
}
104+
#endif
71105
}

src/EasyAppDev.Blazor.Store/DevTools/DevToolsMiddleware.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#if DEBUG
12
using Microsoft.Extensions.Logging;
23
using Microsoft.JSInterop;
34
using System.Text.Json;
@@ -10,6 +11,8 @@ namespace EasyAppDev.Blazor.Store.DevTools;
1011
/// Middleware that integrates with Redux DevTools browser extension.
1112
/// Uses lazy IJSRuntime resolution via IServiceProvider for compatibility
1213
/// with Blazor Server, WebAssembly, and Auto render modes.
14+
/// IMPORTANT: This middleware is only available in DEBUG builds for security reasons.
15+
/// DevTools expose your application state and should never be used in production.
1316
/// </summary>
1417
/// <typeparam name="TState">The type of state managed by the store.</typeparam>
1518
public class DevToolsMiddleware<TState> : IMiddleware<TState>, IAsyncDisposable
@@ -188,3 +191,54 @@ public async ValueTask DisposeAsync()
188191
GC.SuppressFinalize(this);
189192
}
190193
}
194+
195+
#else
196+
197+
using EasyAppDev.Blazor.Store.Middleware;
198+
199+
namespace EasyAppDev.Blazor.Store.DevTools;
200+
201+
/// <summary>
202+
/// No-op DevTools middleware stub for Release builds.
203+
/// DevTools are disabled in production for security reasons.
204+
/// </summary>
205+
/// <typeparam name="TState">The type of state managed by the store.</typeparam>
206+
public class DevToolsMiddleware<TState> : IMiddleware<TState>, IAsyncDisposable
207+
where TState : notnull
208+
{
209+
/// <summary>
210+
/// No-op constructor for Release builds.
211+
/// </summary>
212+
public DevToolsMiddleware(
213+
IServiceProvider serviceProvider,
214+
string storeName = "Store",
215+
object? logger = null)
216+
{
217+
}
218+
219+
/// <summary>
220+
/// No-op constructor for Release builds.
221+
/// </summary>
222+
public DevToolsMiddleware(
223+
IServiceProvider serviceProvider,
224+
string storeName,
225+
object? options,
226+
object? logger = null)
227+
{
228+
}
229+
230+
/// <inheritdoc />
231+
public Task OnBeforeUpdateAsync(TState currentState, string? action) => Task.CompletedTask;
232+
233+
/// <inheritdoc />
234+
public Task OnAfterUpdateAsync(TState previousState, TState currentState, string? action) => Task.CompletedTask;
235+
236+
/// <inheritdoc />
237+
public ValueTask DisposeAsync()
238+
{
239+
GC.SuppressFinalize(this);
240+
return ValueTask.CompletedTask;
241+
}
242+
}
243+
244+
#endif

src/EasyAppDev.Blazor.Store/DevTools/DevToolsOptions.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ public class DevToolsOptions<TState> where TState : notnull
1919

2020
/// <summary>
2121
/// Gets or sets whether time-travel debugging is enabled.
22-
/// Default is true.
22+
/// Default is false for security. Time-travel allows jumping to previous states,
23+
/// which can expose historical sensitive data.
2324
/// </summary>
24-
public bool EnableTimeTravel { get; set; } = true;
25+
public bool EnableTimeTravel { get; set; } = false;
2526

2627
/// <summary>
2728
/// Gets or sets the maximum number of actions to keep in history.
@@ -31,13 +32,15 @@ public class DevToolsOptions<TState> where TState : notnull
3132

3233
/// <summary>
3334
/// Gets or sets whether action replay is enabled.
34-
/// Default is true.
35+
/// Default is false for security. Action replay allows re-executing actions,
36+
/// which could have unintended side effects in production environments.
3537
/// </summary>
36-
public bool EnableActionReplay { get; set; } = true;
38+
public bool EnableActionReplay { get; set; } = false;
3739

3840
/// <summary>
3941
/// Gets or sets whether state editing from DevTools is enabled.
40-
/// Default is false for safety.
42+
/// Default is false for security. State editing allows arbitrary state modifications
43+
/// from the browser DevTools, which is a serious security risk in production.
4144
/// </summary>
4245
public bool EnableStateEditing { get; set; } = false;
4346

0 commit comments

Comments
 (0)