Files
git.stella-ops.org/src/Notifier/StellaOps.Notifier/StellaOps.Notifier.Tests/Security/HtmlSanitizerTests.cs
master e950474a77
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
up
2025-11-27 15:16:31 +02:00

372 lines
9.1 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Notifier.Worker.Security;
namespace StellaOps.Notifier.Tests.Security;
public class HtmlSanitizerTests
{
private readonly HtmlSanitizerOptions _options;
private readonly DefaultHtmlSanitizer _sanitizer;
public HtmlSanitizerTests()
{
_options = new HtmlSanitizerOptions
{
DefaultProfile = "basic",
LogSanitization = false
};
_sanitizer = new DefaultHtmlSanitizer(
Options.Create(_options),
NullLogger<DefaultHtmlSanitizer>.Instance);
}
[Fact]
public void Sanitize_AllowedTags_Preserved()
{
// Arrange
var html = "<p>Hello <strong>World</strong></p>";
// Act
var result = _sanitizer.Sanitize(html);
// Assert
Assert.Contains("<p>", result);
Assert.Contains("<strong>", result);
Assert.Contains("</strong>", result);
Assert.Contains("</p>", result);
}
[Fact]
public void Sanitize_DisallowedTags_Removed()
{
// Arrange
var html = "<p>Hello</p><iframe src='evil.com'></iframe>";
// Act
var result = _sanitizer.Sanitize(html);
// Assert
Assert.Contains("<p>Hello</p>", result);
Assert.DoesNotContain("<iframe", result);
}
[Fact]
public void Sanitize_ScriptTags_Removed()
{
// Arrange
var html = "<p>Hello</p><script>alert('xss')</script>";
// Act
var result = _sanitizer.Sanitize(html);
// Assert
Assert.Contains("<p>Hello</p>", result);
Assert.DoesNotContain("<script", result);
Assert.DoesNotContain("alert", result);
}
[Fact]
public void Sanitize_EventHandlers_Removed()
{
// Arrange
var html = "<p onclick='alert(1)'>Hello</p>";
// Act
var result = _sanitizer.Sanitize(html);
// Assert
Assert.DoesNotContain("onclick", result);
Assert.Contains("<p>Hello</p>", result);
}
[Fact]
public void Sanitize_JavaScriptUrls_Removed()
{
// Arrange
var html = "<a href='javascript:alert(1)'>Click</a>";
// Act
var result = _sanitizer.Sanitize(html);
// Assert
Assert.DoesNotContain("javascript:", result);
}
[Fact]
public void Sanitize_AllowedAttributes_Preserved()
{
// Arrange
var html = "<a href='https://example.com' title='Example'>Link</a>";
// Act
var result = _sanitizer.Sanitize(html);
// Assert
Assert.Contains("href=", result);
Assert.Contains("https://example.com", result);
Assert.Contains("title=", result);
}
[Fact]
public void Sanitize_DisallowedAttributes_Removed()
{
// Arrange
var html = "<p data-custom='value' class='test'>Hello</p>";
// Act
var result = _sanitizer.Sanitize(html);
// Assert
Assert.DoesNotContain("data-custom", result);
Assert.Contains("class=", result); // class is allowed
}
[Fact]
public void Sanitize_WithMinimalProfile_OnlyBasicTags()
{
// Arrange
var html = "<p><a href='https://example.com'>Link</a></p>";
var profile = SanitizationProfile.Minimal;
// Act
var result = _sanitizer.Sanitize(html, profile);
// Assert
Assert.Contains("<p>", result);
Assert.DoesNotContain("<a", result); // links not in minimal
}
[Fact]
public void Sanitize_WithRichProfile_AllowsImagesAndTables()
{
// Arrange
var html = "<table><tr><td>Cell</td></tr></table><img src='test.png' alt='Test'>";
var profile = SanitizationProfile.Rich;
// Act
var result = _sanitizer.Sanitize(html, profile);
// Assert
Assert.Contains("<table>", result);
Assert.Contains("<img", result);
Assert.Contains("src=", result);
}
[Fact]
public void Sanitize_HtmlComments_Removed()
{
// Arrange
var html = "<p>Hello<!-- comment --></p>";
// Act
var result = _sanitizer.Sanitize(html);
// Assert
Assert.DoesNotContain("<!--", result);
Assert.DoesNotContain("comment", result);
}
[Fact]
public void Sanitize_EmptyString_ReturnsEmpty()
{
// Act
var result = _sanitizer.Sanitize("");
// Assert
Assert.Empty(result);
}
[Fact]
public void Sanitize_NullString_ReturnsNull()
{
// Act
var result = _sanitizer.Sanitize(null!);
// Assert
Assert.Null(result);
}
[Fact]
public void Validate_SafeHtml_ReturnsValid()
{
// Arrange
var html = "<p>Hello <strong>World</strong></p>";
// Act
var result = _sanitizer.Validate(html);
// Assert
Assert.True(result.IsValid);
Assert.Empty(result.Errors);
}
[Fact]
public void Validate_ScriptTag_ReturnsErrors()
{
// Arrange
var html = "<script>alert('xss')</script>";
// Act
var result = _sanitizer.Validate(html);
// Assert
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.Type == HtmlValidationErrorType.ScriptDetected);
Assert.True(result.ContainedDangerousContent);
}
[Fact]
public void Validate_EventHandler_ReturnsErrors()
{
// Arrange
var html = "<p onclick='alert(1)'>Hello</p>";
// Act
var result = _sanitizer.Validate(html);
// Assert
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.Type == HtmlValidationErrorType.EventHandlerDetected);
}
[Fact]
public void Validate_JavaScriptUrl_ReturnsErrors()
{
// Arrange
var html = "<a href='javascript:void(0)'>Click</a>";
// Act
var result = _sanitizer.Validate(html);
// Assert
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.Type == HtmlValidationErrorType.JavaScriptUrlDetected);
}
[Fact]
public void Validate_DisallowedTags_ReturnsWarnings()
{
// Arrange
var html = "<p>Hello</p><custom-tag>Custom</custom-tag>";
// Act
var result = _sanitizer.Validate(html);
// Assert
Assert.Contains(result.RemovedTags, t => t == "custom-tag");
}
[Fact]
public void EscapeHtml_EscapesSpecialCharacters()
{
// Arrange
var text = "<script>alert('test')</script>";
// Act
var result = _sanitizer.EscapeHtml(text);
// Assert
Assert.DoesNotContain("<", result);
Assert.DoesNotContain(">", result);
Assert.Contains("&lt;", result);
Assert.Contains("&gt;", result);
}
[Fact]
public void StripTags_RemovesAllTags()
{
// Arrange
var html = "<p>Hello <strong>World</strong></p>";
// Act
var result = _sanitizer.StripTags(html);
// Assert
Assert.DoesNotContain("<", result);
Assert.DoesNotContain(">", result);
Assert.Contains("Hello", result);
Assert.Contains("World", result);
}
[Fact]
public void GetProfile_ExistingProfile_ReturnsProfile()
{
// Act
var profile = _sanitizer.GetProfile("basic");
// Assert
Assert.NotNull(profile);
Assert.Equal("basic", profile.Name);
}
[Fact]
public void GetProfile_NonExistentProfile_ReturnsNull()
{
// Act
var profile = _sanitizer.GetProfile("non-existent");
// Assert
Assert.Null(profile);
}
[Fact]
public void RegisterProfile_AddsCustomProfile()
{
// Arrange
var customProfile = new SanitizationProfile
{
Name = "custom",
AllowedTags = new HashSet<string> { "p", "custom-tag" }
};
// Act
_sanitizer.RegisterProfile("custom", customProfile);
var retrieved = _sanitizer.GetProfile("custom");
// Assert
Assert.NotNull(retrieved);
Assert.Equal("custom", retrieved.Name);
}
[Theory]
[InlineData("<p>Test</p>", "<p>Test</p>")]
[InlineData("<P>Test</P>", "<p>Test</p>")]
[InlineData("<DIV>Test</DIV>", "<div>Test</div>")]
public void Sanitize_NormalizesTagCase(string input, string expected)
{
// Act
var result = _sanitizer.Sanitize(input);
// Assert
Assert.Equal(expected, result.Trim());
}
[Fact]
public void Sanitize_SafeUrlSchemes_Preserved()
{
// Arrange
var html = "<a href='mailto:test@example.com'>Email</a>";
// Act
var result = _sanitizer.Sanitize(html);
// Assert
Assert.Contains("mailto:", result);
}
[Fact]
public void Sanitize_DataUrl_RemovedByDefault()
{
// Arrange
var html = "<img src='data:image/png;base64,abc123' />";
var profile = SanitizationProfile.Rich with { AllowDataUrls = false };
// Act
var result = _sanitizer.Sanitize(html, profile);
// Assert
Assert.DoesNotContain("data:", result);
}
}