Skip to content

Commit 316a913

Browse files
committed
feat: Implement CSS sanitization utility to prevent injection attacks
- Added `CssSanitizer` class with methods to sanitize CSS values including colors, lengths, fonts, shadows, and generic values. - Introduced regex patterns and allowlists for validating safe CSS inputs. - Implemented tests for sanitization methods to ensure malicious inputs are rejected and valid inputs are accepted. refactor: Update ThemingBenchmarks to remove JSRuntime dependency - Simplified `BuildCustomPropertyStyle_SimpleTheme` and `BuildCustomPropertyStyle_ComplexTheme` benchmarks by removing mock JSRuntime usage. test: Add comprehensive security tests for CSS sanitization - Created `SecurityTests` class to validate the behavior of `CssSanitizer` against various malicious and valid CSS inputs. - Ensured that all sanitization methods are covered with appropriate test cases. chore: Remove outdated ThemeManagerTests - Deleted `ThemeManagerTests` as the theme loading mechanism has been refactored to use CSS classes only, making these tests obsolete. chore: Clean up ThemingComponentTests by removing JS theme loading tests - Removed tests related to JS theme loading as the implementation has shifted to CSS class-based theming.
1 parent 3208198 commit 316a913

13 files changed

Lines changed: 897 additions & 682 deletions

File tree

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,66 @@ Three source generators ensure AOT compatibility and developer productivity:
10311031
2. **ExpressionValidatorGenerator** - Build-time expression validation with diagnostics
10321032
3. **ConfigurationApplierGenerator** - Auto-generates configuration application (100% coverage)
10331033

1034+
## Security
1035+
1036+
This component implements multiple security layers to protect against common web vulnerabilities:
1037+
1038+
### Built-in Protections
1039+
1040+
- **CSS Injection Prevention**: All theme customization values are sanitized using allowlist-based validation. Dangerous patterns like `url()`, `expression()`, `<script>`, and CSS injection attempts are automatically blocked.
1041+
- **Memory Exhaustion Protection**: Search input is limited to 2,000 characters maximum to prevent DoS attacks through excessive memory allocation in filtering algorithms.
1042+
- **XSS Protection**: Template content and user input are handled through Blazor's built-in encoding mechanisms.
1043+
- **SIMD Optimization**: AI similarity calculations use hardware-accelerated operations with bounded memory allocation.
1044+
1045+
### Configuration Limits
1046+
1047+
```csharp
1048+
<AutoComplete TItem="Product"
1049+
MaxSearchLength="500" // Default: 500, Max: 2000
1050+
... />
1051+
```
1052+
1053+
The `MaxSearchLength` parameter allows you to configure the maximum allowed search text length. This prevents:
1054+
- Excessive memory allocation in fuzzy matching algorithms (Levenshtein distance)
1055+
- Performance degradation from processing extremely long search strings
1056+
- Potential denial-of-service attacks
1057+
1058+
### Theme Customization Safety
1059+
1060+
All CSS custom properties are validated before rendering:
1061+
1062+
```csharp
1063+
// ✅ Safe - Valid CSS values pass through
1064+
ThemeOverrides = new ThemeOptions {
1065+
Colors = new ColorOptions { Primary = "#FF6B6B" },
1066+
Spacing = new SpacingOptions { BorderRadius = "8px" }
1067+
}
1068+
1069+
// ❌ Blocked - Malicious values are rejected
1070+
ThemeOverrides = new ThemeOptions {
1071+
Colors = new ColorOptions { Primary = "url(javascript:alert(1))" } // Rejected
1072+
}
1073+
```
1074+
1075+
Supported CSS value types:
1076+
- **Colors**: hex, rgb/rgba, hsl/hsla, named colors
1077+
- **Lengths**: px, em, rem, %, vh, vw (including multi-value like `"10px 20px"`)
1078+
- **Fonts**: Standard font families and generic families
1079+
- **Shadows**: box-shadow and text-shadow syntax
1080+
- **Times**: ms and s units
1081+
1082+
### Security Best Practices
1083+
1084+
1. **Use the latest version** to ensure you have all security patches
1085+
2. **Validate user data** on the server side before passing to the component
1086+
3. **Set appropriate `MaxSearchLength`** based on your use case (default 500 is recommended)
1087+
4. **Review theme customizations** if accepting user-provided theme values
1088+
5. **Use CSP headers** for additional XSS protection in production
1089+
1090+
### Reporting Security Issues
1091+
1092+
If you discover a security vulnerability, please email [email protected] instead of using the public issue tracker.
1093+
10341094
## Contributing
10351095

10361096
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.

src/EasyAppDev.Blazor.AutoComplete/Components/AutoComplete.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
id="@_inputIdValue"
3030
class="ebd-ac-input"
3131
placeholder="@Placeholder"
32+
maxlength="@GetEffectiveMaxSearchLength()"
3233
@bind-value="_searchText"
3334
@bind-value:event="oninput"
3435
@bind-value:after="OnSearchTextChangedAsync"

src/EasyAppDev.Blazor.AutoComplete/Components/AutoComplete.razor.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,14 @@ public partial class AutoComplete<TItem> : ComponentBase, IAsyncDisposable
223223
[Parameter]
224224
public int DebounceMs { get; set; } = 300;
225225

226+
/// <summary>
227+
/// Maximum allowed length for search text to prevent memory exhaustion attacks.
228+
/// Default is 500 characters. Maximum allowed value is 2000.
229+
/// Values exceeding this limit will be truncated.
230+
/// </summary>
231+
[Parameter]
232+
public int MaxSearchLength { get; set; } = 500;
233+
226234
/// <summary>
227235
/// Whether to enable virtualization for large datasets.
228236
/// </summary>
@@ -511,6 +519,13 @@ private void ApplyConfiguration(AutoCompleteConfig<TItem> config)
511519

512520
private async Task OnSearchTextChangedAsync()
513521
{
522+
// Security: Enforce maximum search length to prevent memory exhaustion
523+
var effectiveMaxLength = GetEffectiveMaxSearchLength();
524+
if (_searchText.Length > effectiveMaxLength)
525+
{
526+
_searchText = _searchText.Substring(0, effectiveMaxLength);
527+
}
528+
514529
if (_searchText.Length >= MinSearchLength)
515530
{
516531
if (_debounceTimer != null)
@@ -691,6 +706,15 @@ private void OnValidationStateChanged(object? sender, ValidationStateChangedEven
691706

692707
#region Helper Methods
693708

709+
/// <summary>
710+
/// Gets the effective maximum search length, enforcing a hard limit of 2000 characters.
711+
/// </summary>
712+
private int GetEffectiveMaxSearchLength()
713+
{
714+
const int AbsoluteMaxLength = 2000;
715+
return Math.Min(MaxSearchLength, AbsoluteMaxLength);
716+
}
717+
694718
private async Task FilterItemsAsync()
695719
{
696720
// Cancel any ongoing load operation

src/EasyAppDev.Blazor.AutoComplete/Filtering/ContainsFilter.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ public IEnumerable<TItem> FilterMultiField(
3636
return items;
3737
}
3838

39+
// Security: Prevent performance degradation from excessively long search strings
40+
const int MaxSearchLength = 2000;
41+
if (searchText.Length > MaxSearchLength)
42+
{
43+
searchText = searchText.Substring(0, MaxSearchLength);
44+
}
45+
3946
var searchLower = searchText.ToLowerInvariant();
4047

4148
return items.Where(item =>

src/EasyAppDev.Blazor.AutoComplete/Filtering/FilterEngineBase.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ public IEnumerable<TItem> Filter(
2424
return items;
2525
}
2626

27+
// Security: Prevent performance degradation from excessively long search strings
28+
const int MaxSearchLength = 2000;
29+
if (searchText.Length > MaxSearchLength)
30+
{
31+
searchText = searchText.Substring(0, MaxSearchLength);
32+
}
33+
2734
// Common case conversion
2835
var searchLower = searchText.ToLowerInvariant();
2936

src/EasyAppDev.Blazor.AutoComplete/Filtering/FuzzyFilter.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ public IEnumerable<TItem> Filter(
3535
return items;
3636
}
3737

38+
// Security: Prevent memory exhaustion from excessively long search strings
39+
const int MaxSearchLength = 2000;
40+
if (searchText.Length > MaxSearchLength)
41+
{
42+
searchText = searchText.Substring(0, MaxSearchLength);
43+
}
44+
3845
var searchLower = searchText.ToLowerInvariant();
3946

4047
return items
@@ -67,6 +74,13 @@ public IEnumerable<TItem> FilterMultiField(
6774
return items;
6875
}
6976

77+
// Security: Prevent memory exhaustion from excessively long search strings
78+
const int MaxSearchLength = 2000;
79+
if (searchText.Length > MaxSearchLength)
80+
{
81+
searchText = searchText.Substring(0, MaxSearchLength);
82+
}
83+
7084
var searchLower = searchText.ToLowerInvariant();
7185

7286
return items
@@ -143,8 +157,14 @@ private static int LevenshteinDistance(string source, string target)
143157
return source.Length;
144158
}
145159

146-
var sourceLength = source.Length;
147-
var targetLength = target.Length;
160+
// Security: Prevent memory exhaustion from excessively long strings
161+
// Matrix allocation = (sourceLength + 1) * (targetLength + 1) * sizeof(int)
162+
// Limit each dimension to 1000 chars to cap memory at ~4MB per calculation
163+
const int MaxLengthForLevenshtein = 1000;
164+
165+
var sourceLength = Math.Min(source.Length, MaxLengthForLevenshtein);
166+
var targetLength = Math.Min(target.Length, MaxLengthForLevenshtein);
167+
148168
var distance = new int[sourceLength + 1, targetLength + 1];
149169

150170
for (var i = 0; i <= sourceLength; distance[i, 0] = i++) { }

src/EasyAppDev.Blazor.AutoComplete/Filtering/StartsWithFilter.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ public IEnumerable<TItem> FilterMultiField(
3535
return items;
3636
}
3737

38+
// Security: Prevent performance degradation from excessively long search strings
39+
const int MaxSearchLength = 2000;
40+
if (searchText.Length > MaxSearchLength)
41+
{
42+
searchText = searchText.Substring(0, MaxSearchLength);
43+
}
44+
3845
var searchLower = searchText.ToLowerInvariant();
3946

4047
return items.Where(item =>

0 commit comments

Comments
 (0)