Add sanitization of CSS at-rules.

This commit is contained in:
Michael Ganss
2016-03-22 19:16:33 +01:00
parent 4e3a64770f
commit d33cb8672b
6 changed files with 249 additions and 21 deletions

View File

@@ -2520,6 +2520,91 @@ rl(javascript:alert(""foo""))'>";
Assert.That(actual, Is.EqualTo("<div>Test</div>"));
}
[Test]
public void StyleTagTest()
{
var s = new HtmlSanitizer();
s.AllowedTags.Add("style");
var html = "<html><head><style>body { background-color: white; hallo-ballo: xyz }</style></head><body><div>Test</div></body></html>";
var actual = s.SanitizeDocument(html);
Assert.That(actual, Is.EqualTo("<html><head><style>body { background-color: white }</style></head><body><div>Test</div></body></html>"));
}
[Test]
public void StyleAtTest()
{
var s = new HtmlSanitizer();
s.AllowedTags.Add("style");
s.AllowedAtRules.Add(AngleSharp.Dom.Css.CssRuleType.Media);
s.AllowedAtRules.Add(AngleSharp.Dom.Css.CssRuleType.Keyframes);
s.AllowedAtRules.Add(AngleSharp.Dom.Css.CssRuleType.Keyframe);
s.AllowedAtRules.Add(AngleSharp.Dom.Css.CssRuleType.Page);
var html = @"<html><head><style>
@charset ""UTF-8"";
@import url(evil.css);
@namespace url(http://www.w3.org/1999/xhtml);
@namespace svg url(http://www.w3.org/2000/svg);
@media (min-width: 100px) {
div { color: black; }
@font-face { font-family: test }
}
@supports (--foo: green) {
body {
color: green;
}
@media (min-width: 200px) {
body { color: red; }
}
}
@document url(http://www.w3.org/),
url-prefix(http://www.w3.org/Style/),
domain(mozilla.org),
regexp(""https:.* "")
{
body {
color: purple;
background: yellow;
}
}
@page { size:8.5in 11in; margin: 2cm }
@font-face {
font-family: ""Bitstream Vera Serif Bold""
src: url(""https://mdn.mozillademos.org/files/2468/VeraSeBd.ttf"");
color: black;
}
@keyframes identifier {
0% { top: 0; }
50% { top: 30px; left: 20px; }
50% { top: 10px; }
100% { top: 0; background: url('javascript:alert(xss)') }
}
@viewport {
min-width: 640px;
max-width: 800px;
}
@counter-style winners-list {
system: fixed;
symbols: url(gold-medal.svg) url(silver-medal.svg) url(bronze-medal.svg);
suffix: "" "";
}
@font-feature-values Font One { /* How to activate nice-style in Font One */
@styleset {
nice-style: 12;
}
}
</style></head></html>";
var actual = s.SanitizeDocument(html);
Assert.That(actual, Is.EqualTo(@"<html><head><style>@namespace url(""http://www.w3.org/1999/xhtml"");
@namespace svg url(""http://www.w3.org/2000/svg"");
@media (min-width: 100px) { div { color: black } }
@page * { margin: 2cm }
@keyframes identifier { 0% { top: 0 } 50% { top: 30px; left: 20px } 50% { top: 10px } 100% { top: 0 } }</style></head><body></body></html>".Replace("\r\n", "\n")));
}
}
}

View File

@@ -130,4 +130,25 @@ namespace Ganss.XSS
public RemoveReason Reason { get; set; }
}
/// <summary>
/// Provides data for the <see cref="HtmlSanitizer.RemovingAtRule"/> event.
/// </summary>
public class RemovingAtRuleEventArgs : CancelEventArgs
{
/// <summary>
/// The tag containing the at-rule to be removed.
/// </summary>
/// <value>
/// The tag.
/// </value>
public IElement Tag { get; set; }
/// <summary>
/// Gets or sets the rule to be removed.
/// </summary>
/// <value>
/// The rule.
/// </value>
public ICssRule Rule { get; set; }
}
}

View File

@@ -67,8 +67,22 @@ namespace Ganss.XSS
AllowedAttributes = new HashSet<string>(allowedAttributes ?? DefaultAllowedAttributes, StringComparer.OrdinalIgnoreCase);
UriAttributes = new HashSet<string>(uriAttributes ?? DefaultUriAttributes, StringComparer.OrdinalIgnoreCase);
AllowedCssProperties = new HashSet<string>(allowedCssProperties ?? DefaultAllowedCssProperties, StringComparer.OrdinalIgnoreCase);
AllowedAtRules = new HashSet<CssRuleType>(DefaultAllowedAtRules);
}
/// <summary>
/// Gets or sets the allowed CSS at-rules such as "@media" and "@font-face".
/// </summary>
/// <value>
/// The allowed CSS at-rules.
/// </value>
public ISet<CssRuleType> AllowedAtRules { get; private set; }
/// <summary>
/// The default allowed CSS at-rules.
/// </summary>
public static readonly ISet<CssRuleType> DefaultAllowedAtRules = new HashSet<CssRuleType>() { CssRuleType.Style, CssRuleType.Namespace };
/// <summary>
/// Gets or sets the allowed HTTP schemes such as "http" and "https".
/// </summary>
@@ -256,6 +270,10 @@ namespace Ganss.XSS
/// Occurs before a style is removed.
/// </summary>
public event EventHandler<RemovingStyleEventArgs> RemovingStyle;
/// <summary>
/// Occurs before an at-rule is removed.
/// </summary>
public event EventHandler<RemovingAtRuleEventArgs> RemovingAtRule;
/// <summary>
/// Raises the <see cref="E:PostProcessNode" /> event.
@@ -293,6 +311,15 @@ namespace Ganss.XSS
if (RemovingStyle != null) RemovingStyle(this, e);
}
/// <summary>
/// Raises the <see cref="E:RemovingAtRule" /> event.
/// </summary>
/// <param name="e">The <see cref="RemovingAtRuleEventArgs"/> instance containing the event data.</param>
protected virtual void OnRemovingAtRule(RemovingAtRuleEventArgs e)
{
if (RemovingAtRule != null) RemovingAtRule(this, e);
}
/// <summary>
/// The default regex for disallowed CSS property values.
/// </summary>
@@ -388,6 +415,8 @@ namespace Ganss.XSS
RemoveTag(tag, RemoveReason.NotAllowedTag);
}
SanitizeStyleSheets(dom, baseUrl);
// cleanup attributes
foreach (var tag in context.QuerySelectorAll("*").OfType<IHtmlElement>().ToList())
{
@@ -433,6 +462,72 @@ namespace Ganss.XSS
DoPostProcess(dom, nodes);
}
private void SanitizeStyleSheets(IHtmlDocument dom, string baseUrl)
{
foreach (var styleSheet in dom.StyleSheets.OfType<ICssStyleSheet>())
{
var styleTag = styleSheet.OwnerNode;
for (int i = 0; i < styleSheet.Rules.Length;)
{
var rule = styleSheet.Rules[i];
if (!SanitizeStyleRule(rule, styleTag, baseUrl) && RemoveAtRule(styleTag, rule))
styleSheet.RemoveAt(i);
else i++;
}
styleTag.InnerHtml = styleSheet.ToCss();
}
}
private bool SanitizeStyleRule(ICssRule rule, IElement styleTag, string baseUrl)
{
if (!AllowedAtRules.Contains(rule.Type)) return false;
var styleRule = rule as ICssStyleRule;
if (styleRule != null)
{
SanitizeStyleDeclaration(styleTag, styleRule.Style, baseUrl);
}
else
{
var groupingRule = rule as ICssGroupingRule;
if (groupingRule != null)
{
for (int i = 0; i < groupingRule.Rules.Length;)
{
var childRule = groupingRule.Rules[i];
if (!SanitizeStyleRule(childRule, styleTag, baseUrl) && RemoveAtRule(styleTag, childRule))
groupingRule.RemoveAt(i);
else i++;
}
}
else if (rule is ICssPageRule)
{
var pageRule = (ICssPageRule)rule;
SanitizeStyleDeclaration(styleTag, pageRule.Style, baseUrl);
}
else if (rule is ICssKeyframesRule)
{
var keyFramesRule = (ICssKeyframesRule)rule;
foreach (var childRule in keyFramesRule.Rules.OfType<ICssKeyframeRule>().ToList())
{
if (!SanitizeStyleRule(childRule, styleTag, baseUrl) && RemoveAtRule(styleTag, childRule))
keyFramesRule.Remove(childRule.KeyText);
}
}
else if (rule is ICssKeyframeRule)
{
var keyFrameRule = (ICssKeyframeRule)rule;
SanitizeStyleDeclaration(styleTag, keyFrameRule.Style, baseUrl);
}
}
return true;
}
/// <summary>
/// Performs post processing on all nodes in the document.
/// </summary>
@@ -499,13 +594,18 @@ namespace Ganss.XSS
protected void SanitizeStyle(IHtmlElement element, string baseUrl)
{
// filter out invalid CSS declarations
// see https://github.com/FlorianRappl/AngleSharp/issues/101
// see https://github.com/AngleSharp/AngleSharp/issues/101
if (element.GetAttribute("style") == null) return;
element.SetAttribute("style", element.Style.ToCss());
var styles = element.Style;
if (styles == null || styles.Length == 0) return;
SanitizeStyleDeclaration(element, styles, baseUrl);
}
private void SanitizeStyleDeclaration(IElement element, ICssStyleDeclaration styles, string baseUrl)
{
var removeStyles = new List<Tuple<ICssProperty, RemoveReason>>();
var setStyles = new Dictionary<string, string>();
@@ -520,7 +620,7 @@ namespace Ganss.XSS
continue;
}
if(CssExpression.IsMatch(val) || DisallowCssPropertyValue.IsMatch(val))
if (CssExpression.IsMatch(val) || DisallowCssPropertyValue.IsMatch(val))
{
removeStyles.Add(new Tuple<ICssProperty, RemoveReason>(style, RemoveReason.NotAllowedValue));
continue;
@@ -547,15 +647,15 @@ namespace Ganss.XSS
}
}
foreach (var style in removeStyles)
{
RemoveStyle(element, styles, style.Item1, style.Item2);
}
foreach (var style in setStyles)
{
styles.SetProperty(style.Key, style.Value);
}
foreach (var style in removeStyles)
{
RemoveStyle(element, styles, style.Item1, style.Item2);
}
}
/// <summary>
@@ -635,10 +735,10 @@ namespace Ganss.XSS
}
/// <summary>
/// Remove a tag from the document.
/// Removes a tag from the document.
/// </summary>
/// <param name="tag">to be removed</param>
/// <param name="reason">reason why to be removed</param>
/// <param name="tag">Tag to be removed</param>
/// <param name="reason">Reason for removal</param>
private void RemoveTag(IElement tag, RemoveReason reason)
{
var e = new RemovingTagEventArgs { Tag = tag, Reason = reason };
@@ -647,11 +747,11 @@ namespace Ganss.XSS
}
/// <summary>
/// Remove an attribute from the document.
/// Removes an attribute from the document.
/// </summary>
/// <param name="tag">tag where the attribute to belongs</param>
/// <param name="attribute">to be removed</param>
/// <param name="reason">reason why to be removed</param>
/// <param name="tag">Tag the attribute belongs to</param>
/// <param name="attribute">Attribute to be removed</param>
/// <param name="reason">Reason for removal</param>
private void RemoveAttribute(IElement tag, IAttr attribute, RemoveReason reason)
{
var e = new RemovingAttributeEventArgs { Tag = tag, Attribute = attribute, Reason = reason };
@@ -660,17 +760,30 @@ namespace Ganss.XSS
}
/// <summary>
/// Remove a style from the document.
/// Removes a style from the document.
/// </summary>
/// <param name="tag">tag where the style belongs</param>
/// <param name="styles">collection where the style to belongs</param>
/// <param name="style">to be removed</param>
/// <param name="reason">reason why to be removed</param>
/// <param name="tag">Tag the style belongs to</param>
/// <param name="styles">Style rule that contains the style to be removed</param>
/// <param name="style">Style to be removed</param>
/// <param name="reason">Reason for removal</param>
private void RemoveStyle(IElement tag, ICssStyleDeclaration styles, ICssProperty style, RemoveReason reason)
{
var e = new RemovingStyleEventArgs { Tag = tag, Style = style, Reason = reason };
OnRemovingStyle(e);
if (!e.Cancel) styles.RemoveProperty(style.Name);
}
/// <summary>
/// Removes an at-rule from the document.
/// </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 bool RemoveAtRule(IElement tag, ICssRule rule)
{
var e = new RemovingAtRuleEventArgs { Tag = tag, Rule = rule };
OnRemovingAtRule(e);
return !e.Cancel;
}
}
}

View File

@@ -2,7 +2,7 @@
<package >
<metadata>
<id>$id$</id>
<version>$version$-beta</version>
<version>$version$</version>
<title>$title$</title>
<authors>$author$</authors>
<owners>$author$</owners>

View File

@@ -24,7 +24,7 @@ namespace Ganss.XSS
/// </summary>
NotAllowedStyle,
/// <summary>
/// Value is a not allowed or harmful url
/// Value is a non-allowed or harmful url
/// </summary>
NotAllowedUrlValue,
/// <summary>

View File

@@ -16,6 +16,7 @@ In order to facilitate different use cases, HtmlSanitizer can be customized at s
- Configure allowed HTML tags through the property `AllowedTags`. All other tags will be stripped.
- Configure allowed HTML attributes through the property `AllowedAttributes`. All other attributes will be stripped.
- Configure allowed CSS property names through the property `AllowedCssProperties`. All other styles will be stripped.
- Configure allowed CSS [at-rules](https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule) through the property `AllowedAtRules`. All other at-rules will be stripped.
- Configure allowed URI schemes through the property `AllowedSchemes`. All other URIs will be stripped.
- Configure HTML attributes that contain URIs (such as "src", "href" etc.) through the property `UriAttributes`.
- Provide a base URI that will be used to resolve relative URIs against.
@@ -38,6 +39,14 @@ var sanitized = sanitizer.Sanitize(html);
### CSS properties allowed by default
`background, background-attachment, background-color, background-image, background-position, background-repeat, border, border-bottom, border-bottom-color, border-bottom-style, border-bottom-width, border-collapse, border-color, border-left, border-left-color, border-left-style, border-left-width, border-right, border-right-color, border-right-style, border-right-width, border-spacing, border-style, border-top, border-top-color, border-top-style, border-top-width, border-width, bottom, caption-side, clear, clip, color, content, counter-increment, counter-reset, cursor, direction, display, empty-cells, float, font, font-family, font-size, font-style, font-variant, font-weight, height, left, letter-spacing, line-height, list-style, list-style-image, list-style-position, list-style-type, margin, margin-bottom, margin-left, margin-right, margin-top, max-height, max-width, min-height, min-width, opacity, orphans, outline, outline-color, outline-style, outline-width, overflow, padding, padding-bottom, padding-left, padding-right, padding-top, page-break-after, page-break-before, page-break-inside, quotes, right, table-layout, text-align, text-decoration, text-indent, text-transform, top, unicode-bidi, vertical-align, visibility, white-space, widows, width, word-spacing, z-index`
### CSS at-rules allowed by default
`namespace, style`
`style` refers to style declarations within other at-rules such as `@media`. Disallowing `@namespace` while allowing other types of at-rules can lead to errors.
Property declarations in `@font-face` and `@viewport` are not sanitized.
_Note:_ the `style` tag is disallowed by default.
### URI schemes allowed by default
``http, https``