Implement CSS class whitelisting
This commit is contained in:
@@ -162,4 +162,34 @@ namespace Ganss.XSS
|
||||
/// </value>
|
||||
public IComment Comment { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="HtmlSanitizer.RemovingCssClass"/> event.
|
||||
/// </summary>
|
||||
public class RemovingCssClassEventArgs : CancelEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the tag containing the CSS class to be removed.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The tag.
|
||||
/// </value>
|
||||
public IElement Tag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the CSS class to be removed.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The CSS class.
|
||||
/// </value>
|
||||
public string CssClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reason why the CSS class will be removed.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The reason.
|
||||
/// </value>
|
||||
public RemoveReason Reason { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,9 @@ namespace Ganss.XSS
|
||||
/// <param name="allowedAttributes">The allowed HTML attributes such as "href" and "alt". When <c>null</c>, uses <see cref="DefaultAllowedAttributes"/></param>
|
||||
/// <param name="uriAttributes">The HTML attributes that can contain a URI such as "href". When <c>null</c>, uses <see cref="DefaultUriAttributes"/></param>
|
||||
/// <param name="allowedCssProperties">The allowed CSS properties such as "font" and "margin". When <c>null</c>, uses <see cref="DefaultAllowedCssProperties"/></param>
|
||||
/// <param name="allowedCssClasses">CSS class names which are allowed in the value of a class attribute. When <c>null</c>, any class names are allowed.</param>
|
||||
public HtmlSanitizer(IEnumerable<string> allowedTags = null, IEnumerable<string> allowedSchemes = null,
|
||||
IEnumerable<string> allowedAttributes = null, IEnumerable<string> uriAttributes = null, IEnumerable<string> allowedCssProperties = null)
|
||||
IEnumerable<string> allowedAttributes = null, IEnumerable<string> uriAttributes = null, IEnumerable<string> allowedCssProperties = null, IEnumerable<string> allowedCssClasses = null)
|
||||
{
|
||||
AllowedTags = new HashSet<string>(allowedTags ?? DefaultAllowedTags, StringComparer.OrdinalIgnoreCase);
|
||||
AllowedSchemes = new HashSet<string>(allowedSchemes ?? DefaultAllowedSchemes, StringComparer.OrdinalIgnoreCase);
|
||||
@@ -68,6 +69,7 @@ namespace Ganss.XSS
|
||||
UriAttributes = new HashSet<string>(uriAttributes ?? DefaultUriAttributes, StringComparer.OrdinalIgnoreCase);
|
||||
AllowedCssProperties = new HashSet<string>(allowedCssProperties ?? DefaultAllowedCssProperties, StringComparer.OrdinalIgnoreCase);
|
||||
AllowedAtRules = new HashSet<CssRuleType>(DefaultAllowedAtRules);
|
||||
AllowedCssClasses = allowedCssClasses != null ? new HashSet<string>(allowedCssClasses) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -284,6 +286,14 @@ namespace Ganss.XSS
|
||||
set { _disallowedCssPropertyValue = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed CSS classes.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The allowed CSS classes.
|
||||
/// </value>
|
||||
public ISet<string> AllowedCssClasses { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs for every node after sanitizing.
|
||||
/// </summary>
|
||||
@@ -308,6 +318,10 @@ namespace Ganss.XSS
|
||||
/// Occurs before a comment is removed.
|
||||
/// </summary>
|
||||
public event EventHandler<RemovingCommentEventArgs> RemovingComment;
|
||||
/// <summary>
|
||||
/// Occurs before a CSS class is removed.
|
||||
/// </summary>
|
||||
public event EventHandler<RemovingCssClassEventArgs> RemovingCssClass;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="E:PostProcessNode" /> event.
|
||||
@@ -368,6 +382,15 @@ namespace Ganss.XSS
|
||||
/// </summary>
|
||||
public static readonly Regex DefaultDisallowedCssPropertyValue = new Regex(@"[<>]", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="E:RemovingCSSClass" /> event.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="RemovingCSSClass"/> instance containing the event data.</param>
|
||||
protected virtual void OnRemovingCssClass(RemovingCssClassEventArgs e)
|
||||
{
|
||||
RemovingCssClass?.Invoke(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return all nested subnodes of a node.
|
||||
/// </summary>
|
||||
@@ -489,15 +512,35 @@ namespace Ganss.XSS
|
||||
// sanitize the style attribute
|
||||
SanitizeStyle(tag, baseUrl);
|
||||
|
||||
var checkClasses = AllowedCssClasses != null;
|
||||
var allowedTags = AllowedCssClasses?.ToArray() ?? new string[0];
|
||||
|
||||
// sanitize the value of the attributes
|
||||
foreach (var attribute in tag.Attributes.ToList())
|
||||
{
|
||||
// The '& Javascript include' is a possible method to execute Javascript and can lead to XSS.
|
||||
// (see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#.26_JavaScript_includes)
|
||||
if (attribute.Value.Contains("&{"))
|
||||
{
|
||||
RemoveAttribute(tag, attribute, RemoveReason.NotAllowedValue);
|
||||
}
|
||||
else
|
||||
tag.SetAttribute(attribute.Name, attribute.Value);
|
||||
{
|
||||
if (checkClasses && attribute.Name == "class")
|
||||
{
|
||||
var removedClasses = tag.ClassList.Except(allowedTags).ToArray();
|
||||
|
||||
foreach(var removedClass in removedClasses)
|
||||
RemoveCssClass(tag, removedClass, RemoveReason.NotAllowedCssClass);
|
||||
|
||||
if (!tag.ClassList.Any())
|
||||
RemoveAttribute(tag, attribute, RemoveReason.ClassAttributeEmpty);
|
||||
}
|
||||
else
|
||||
{
|
||||
tag.SetAttribute(attribute.Name, attribute.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,5 +877,18 @@ namespace Ganss.XSS
|
||||
OnRemovingAtRule(e);
|
||||
return !e.Cancel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a CSS class from a class attribute.
|
||||
/// </summary>
|
||||
/// <param name="tag">Tag the style belongs to</param>
|
||||
/// <param name="rule">Rule to be removed</param>
|
||||
/// <returns>true, if the rule can be removed; false, otherwise.</returns>
|
||||
private void RemoveCssClass(IElement tag, string cssClass, RemoveReason reason)
|
||||
{
|
||||
var e = new RemovingCssClassEventArgs { Tag = tag, CssClass = cssClass, Reason = reason };
|
||||
OnRemovingCssClass(e);
|
||||
if (!e.Cancel) tag.ClassList.Remove(cssClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,13 @@ namespace Ganss.XSS
|
||||
/// </value>
|
||||
Regex DisallowCssPropertyValue { get; set; }
|
||||
|
||||
/// Gets or sets the allowed CSS classes.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The allowed CSS classes.
|
||||
/// </value>
|
||||
ISet<string> AllowedCssClasses { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs for every node after sanitizing.
|
||||
/// </summary>
|
||||
@@ -84,6 +91,11 @@ namespace Ganss.XSS
|
||||
/// </summary>
|
||||
event EventHandler<RemovingStyleEventArgs> RemovingStyle;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before a CSS class is removed.
|
||||
/// </summary>
|
||||
event EventHandler<RemovingCssClassEventArgs> RemovingCssClass;
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes the specified HTML.
|
||||
/// </summary>
|
||||
|
||||
@@ -25,5 +25,13 @@
|
||||
/// Value is not allowed or harmful
|
||||
/// </summary>
|
||||
NotAllowedValue,
|
||||
/// <summary>
|
||||
/// CSS Class is not allowed
|
||||
/// </summary>
|
||||
NotAllowedCssClass,
|
||||
/// <summary>
|
||||
/// The class attribute is empty
|
||||
/// </summary>
|
||||
ClassAttributeEmpty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2499,6 +2499,44 @@ rl(javascript:alert(""foo""))'>";
|
||||
Assert.Equal(RemoveReason.NotAllowedTag, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveEventForNotAllowedCssClass()
|
||||
{
|
||||
RemoveReason? reason = null;
|
||||
string removedClass = null;
|
||||
|
||||
var s = new HtmlSanitizer(allowedAttributes: new[] { "class" }, allowedCssClasses: new[] { "good" });
|
||||
s.RemovingCssClass += (sender, args) =>
|
||||
{
|
||||
reason = args.Reason;
|
||||
removedClass = args.CssClass;
|
||||
};
|
||||
|
||||
s.Sanitize(@"<div class=""good bad"">Test</div>");
|
||||
|
||||
Assert.Equal("bad", removedClass);
|
||||
Assert.Equal(RemoveReason.NotAllowedCssClass, reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveEventForEmptyClassAttributeAfterClassRemoval()
|
||||
{
|
||||
RemoveReason? reason = null;
|
||||
string attributeName = null;
|
||||
|
||||
var s = new HtmlSanitizer(allowedAttributes: new[] { "class" }, allowedCssClasses: new[] { "other" });
|
||||
s.RemovingAttribute += (sender, args) =>
|
||||
{
|
||||
attributeName = args.Attribute.Name;
|
||||
reason = args.Reason;
|
||||
};
|
||||
|
||||
s.Sanitize(@"<div class=""good bad"">Test</div>");
|
||||
|
||||
Assert.Equal("class", attributeName);
|
||||
Assert.Equal(RemoveReason.ClassAttributeEmpty, reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DocumentTest()
|
||||
{
|
||||
@@ -2844,9 +2882,31 @@ zqy1QY1kkPOuMvKWvvmFIwClI2393jVVcp91eda4+J+fIYDbfJa7RY5YcNrZhTuV//9k="">
|
||||
Assert.Equal(0, failures);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllowAllClassesByDefaultTest()
|
||||
{
|
||||
var sanitizer = new HtmlSanitizer(allowedAttributes: new[] { "class" });
|
||||
|
||||
var html = @"<div class=""good bad"">Test</div>";
|
||||
var actual = sanitizer.Sanitize(html);
|
||||
|
||||
Assert.Equal(@"<div class=""good bad"">Test</div>", actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllowClassesTest()
|
||||
{
|
||||
var sanitizer = new HtmlSanitizer(allowedAttributes: new[] { "class" }, allowedCssClasses: new[] { "good" });
|
||||
|
||||
var html = @"<div class=""good bad"">Test</div>";
|
||||
var actual = sanitizer.Sanitize(html);
|
||||
|
||||
Assert.Equal(@"<div class=""good"">Test</div>", actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllowClassesUsingEventTest()
|
||||
{
|
||||
var sanitizer = new HtmlSanitizer();
|
||||
sanitizer.RemovingAttribute += (s, e) =>
|
||||
@@ -2864,6 +2924,29 @@ zqy1QY1kkPOuMvKWvvmFIwClI2393jVVcp91eda4+J+fIYDbfJa7RY5YcNrZhTuV//9k="">
|
||||
Assert.Equal(@"<div class=""good"">Test</div>", actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveClassAttributeIfNoAllowedClassesTest()
|
||||
{
|
||||
// Empty array for allowed classes = no classes allowed
|
||||
var sanitizer = new HtmlSanitizer(allowedAttributes: new[] { "class" }, allowedCssClasses: new string[0]);
|
||||
|
||||
var html = @"<div class=""good bad"">Test</div>";
|
||||
var actual = sanitizer.Sanitize(html);
|
||||
|
||||
Assert.Equal(@"<div>Test</div>", actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveClassAttributeIfEmptyTest()
|
||||
{
|
||||
var sanitizer = new HtmlSanitizer(allowedAttributes: new[] { "class" }, allowedCssClasses: new[] { "other" });
|
||||
|
||||
var html = @"<div class=""good bad"">Test</div>";
|
||||
var actual = sanitizer.Sanitize(html);
|
||||
|
||||
Assert.Equal(@"<div>Test</div>", actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TextTest()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user