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
372 lines
9.1 KiB
C#
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("<", result);
|
|
Assert.Contains(">", 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);
|
|
}
|
|
}
|