manual LF fix
This commit is contained in:
@@ -1,96 +1,96 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{55D772A0-8D8C-4CF7-A876-E6DAB8ED42C0}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Ganss.XSS.Tests</RootNamespace>
|
||||
<AssemblyName>HtmlSanitizer.Tests</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
|
||||
<IsCodedUITest>False</IsCodedUITest>
|
||||
<TestProjectType>UnitTest</TestProjectType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="CsQuery, Version=1.3.3.249, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\CsQuery.1.3.4\lib\net40\CsQuery.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="nunit.framework, Version=2.6.3.13283, Culture=neutral, PublicKeyToken=96d09a1eb7f44a77, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\NUnit.2.6.3\lib\nunit.framework.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
</ItemGroup>
|
||||
<Choose>
|
||||
<When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'">
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
</ItemGroup>
|
||||
</When>
|
||||
<Otherwise />
|
||||
</Choose>
|
||||
<ItemGroup>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Tests.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HtmlSanitizer\HtmlSanitizer.csproj">
|
||||
<Project>{ccdb0c26-d683-4943-b5d8-ac07116461e5}</Project>
|
||||
<Name>HtmlSanitizer</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Choose>
|
||||
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.VisualStudio.QualityTools.CodedUITestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Extension, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestTools.UITesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
</Choose>
|
||||
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{55D772A0-8D8C-4CF7-A876-E6DAB8ED42C0}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Ganss.XSS.Tests</RootNamespace>
|
||||
<AssemblyName>HtmlSanitizer.Tests</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
|
||||
<IsCodedUITest>False</IsCodedUITest>
|
||||
<TestProjectType>UnitTest</TestProjectType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="CsQuery, Version=1.3.3.249, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\CsQuery.1.3.4\lib\net40\CsQuery.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="nunit.framework, Version=2.6.3.13283, Culture=neutral, PublicKeyToken=96d09a1eb7f44a77, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\NUnit.2.6.3\lib\nunit.framework.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
</ItemGroup>
|
||||
<Choose>
|
||||
<When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'">
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
</ItemGroup>
|
||||
</When>
|
||||
<Otherwise />
|
||||
</Choose>
|
||||
<ItemGroup>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Tests.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HtmlSanitizer\HtmlSanitizer.csproj">
|
||||
<Project>{ccdb0c26-d683-4943-b5d8-ac07116461e5}</Project>
|
||||
<Name>HtmlSanitizer</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Choose>
|
||||
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.VisualStudio.QualityTools.CodedUITestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Extension, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestTools.UITesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
</Choose>
|
||||
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
-->
|
||||
</Project>
|
||||
@@ -1,8 +1,8 @@
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using System.Linq;
|
||||
|
||||
// Tests based on tests from http://roadkill.codeplex.com/
|
||||
|
||||
// To create unit tests in this class reference is taken from
|
||||
@@ -2073,33 +2073,33 @@ rl(javascript:alert(""foo""))'>";
|
||||
// Assert
|
||||
string expected = "<BR>";
|
||||
Assert.That(actual, Is.EqualTo(expected).IgnoreCase);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AllowDataAttributesTest()
|
||||
{
|
||||
var sanitizer = new HtmlSanitizer();
|
||||
sanitizer.AllowDataAttributes = true;
|
||||
var html = @"<div data-test1=""value x""></div>";
|
||||
Assert.That(sanitizer.Sanitize(html), Is.EqualTo(html).IgnoreCase);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AllowDataAttributesCaseTest()
|
||||
{
|
||||
var sanitizer = new HtmlSanitizer();
|
||||
sanitizer.AllowDataAttributes = true;
|
||||
var html = @"<div DAta-test1=""value x""></div>";
|
||||
Assert.That(sanitizer.Sanitize(html), Is.EqualTo(html).IgnoreCase);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AllowDataAttributesOffTest()
|
||||
{
|
||||
var sanitizer = new HtmlSanitizer();
|
||||
sanitizer.AllowDataAttributes = false;
|
||||
var html = @"<div data-test1=""value x""></div>";
|
||||
Assert.That(sanitizer.Sanitize(html), Is.EqualTo(@"<div></div>").IgnoreCase);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AllowDataAttributesTest()
|
||||
{
|
||||
var sanitizer = new HtmlSanitizer();
|
||||
sanitizer.AllowDataAttributes = true;
|
||||
var html = @"<div data-test1=""value x""></div>";
|
||||
Assert.That(sanitizer.Sanitize(html), Is.EqualTo(html).IgnoreCase);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AllowDataAttributesCaseTest()
|
||||
{
|
||||
var sanitizer = new HtmlSanitizer();
|
||||
sanitizer.AllowDataAttributes = true;
|
||||
var html = @"<div DAta-test1=""value x""></div>";
|
||||
Assert.That(sanitizer.Sanitize(html), Is.EqualTo(html).IgnoreCase);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AllowDataAttributesOffTest()
|
||||
{
|
||||
var sanitizer = new HtmlSanitizer();
|
||||
sanitizer.AllowDataAttributes = false;
|
||||
var html = @"<div data-test1=""value x""></div>";
|
||||
Assert.That(sanitizer.Sanitize(html), Is.EqualTo(@"<div></div>").IgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +1,94 @@
|
||||
using CsQuery;
|
||||
using CsQuery.Implementation;
|
||||
using CsQuery.Output;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Ganss.XSS
|
||||
{
|
||||
/// <summary>
|
||||
/// Cleans HTML fragments from constructs that can lead to <a href="https://en.wikipedia.org/wiki/Cross-site_scripting">XSS attacks</a>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// XSS attacks can occur at several levels within an HTML fragment:
|
||||
/// <list type="bullet">
|
||||
/// <item>HTML Tags (e.g. the <script> tag)</item>
|
||||
/// <item>HTML attributes (e.g. the "onload" attribute)</item>
|
||||
/// <item>CSS styles (url property values)</item>
|
||||
/// <item>malformed HTML or HTML that exploits parser bugs in specific browsers</item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// The HtmlSanitizer class addresses all of these possible attack vectors by using an HTML parser that is based on the one used
|
||||
/// in the Gecko browser engine (see <a href="https://github.com/jamietre/CsQuery">CsQuery</a>).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In order to facilitate different use cases, HtmlSanitizer can be customized at the levels mentioned above:
|
||||
/// <list type="bullet">
|
||||
/// <item>You can specify the allowed HTML tags through the property <see cref="AllowedTags"/>. All other tags will be stripped.</item>
|
||||
/// <item>You can specify the allowed HTML attributes through the property <see cref="AllowedAttributes"/>. All other attributes will be stripped.</item>
|
||||
/// <item>You can specify the allowed CSS property names through the property <see cref="AllowedCssProperties"/>. All other styles will be stripped.</item>
|
||||
/// <item>You can specify the allowed URI schemes through the property <see cref="AllowedCssProperties"/>. All other URIs will be stripped.</item>
|
||||
/// <item>You can specify the HTML attributes that contain URIs (such as "src", "href" etc.) through the property <see cref="UriAttributes"/>.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// <![CDATA[
|
||||
/// var sanitizer = new HtmlSanitizer();
|
||||
/// var html = @"<script>alert('xss')</script><div onload=""alert('xss')"" style=""background-color: test"">Test<img src=""test.gif"" style=""background-image: url(javascript:alert('xss')); margin: 10px""></div>";
|
||||
/// var sanitized = sanitizer.Sanitize(html, "http://www.example.com");
|
||||
/// // -> "<div style="background-color: test">Test<img style="margin: 10px" src="http://www.example.com/test.gif"></div>"
|
||||
/// ]]>
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class HtmlSanitizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HtmlSanitizer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="allowedTags">The allowed tag names such as "a" and "div". When <c>null</c>, uses <see cref="DefaultAllowedTags"/></param>
|
||||
/// <param name="allowedSchemes">The allowed HTTP schemes such as "http" and "https". When <c>null</c>, uses <see cref="DefaultAllowedSchemes"/></param>
|
||||
/// <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>
|
||||
public HtmlSanitizer(IEnumerable<string> allowedTags = null, IEnumerable<string> allowedSchemes = null,
|
||||
IEnumerable<string> allowedAttributes = null, IEnumerable<string> uriAttributes = null, IEnumerable<string> allowedCssProperties = null)
|
||||
{
|
||||
AllowedTags = new HashSet<string>(allowedTags ?? DefaultAllowedTags, StringComparer.OrdinalIgnoreCase);
|
||||
AllowedSchemes = new HashSet<string>(allowedSchemes ?? DefaultAllowedSchemes, StringComparer.OrdinalIgnoreCase);
|
||||
AllowedAttributes = new HashSet<string>(allowedAttributes ?? DefaultAllowedAttributes, StringComparer.OrdinalIgnoreCase);
|
||||
UriAttributes = new HashSet<string>(uriAttributes ?? DefaultUriAttributes, StringComparer.OrdinalIgnoreCase);
|
||||
AllowedCssProperties = new HashSet<string>(allowedCssProperties ?? DefaultAllowedCssProperties, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed HTTP schemes such as "http" and "https".
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The allowed HTTP schemes.
|
||||
/// </value>
|
||||
public ISet<string> AllowedSchemes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default allowed URI schemes.
|
||||
/// </summary>
|
||||
public static readonly ISet<string> DefaultAllowedSchemes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "http", "https" };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed HTML tag names such as "a" and "div".
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The allowed tag names.
|
||||
/// </value>
|
||||
public ISet<string> AllowedTags { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default allowed HTML tag names.
|
||||
/// </summary>
|
||||
using CsQuery;
|
||||
using CsQuery.Implementation;
|
||||
using CsQuery.Output;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Ganss.XSS
|
||||
{
|
||||
/// <summary>
|
||||
/// Cleans HTML fragments from constructs that can lead to <a href="https://en.wikipedia.org/wiki/Cross-site_scripting">XSS attacks</a>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// XSS attacks can occur at several levels within an HTML fragment:
|
||||
/// <list type="bullet">
|
||||
/// <item>HTML Tags (e.g. the <script> tag)</item>
|
||||
/// <item>HTML attributes (e.g. the "onload" attribute)</item>
|
||||
/// <item>CSS styles (url property values)</item>
|
||||
/// <item>malformed HTML or HTML that exploits parser bugs in specific browsers</item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// The HtmlSanitizer class addresses all of these possible attack vectors by using an HTML parser that is based on the one used
|
||||
/// in the Gecko browser engine (see <a href="https://github.com/jamietre/CsQuery">CsQuery</a>).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// In order to facilitate different use cases, HtmlSanitizer can be customized at the levels mentioned above:
|
||||
/// <list type="bullet">
|
||||
/// <item>You can specify the allowed HTML tags through the property <see cref="AllowedTags"/>. All other tags will be stripped.</item>
|
||||
/// <item>You can specify the allowed HTML attributes through the property <see cref="AllowedAttributes"/>. All other attributes will be stripped.</item>
|
||||
/// <item>You can specify the allowed CSS property names through the property <see cref="AllowedCssProperties"/>. All other styles will be stripped.</item>
|
||||
/// <item>You can specify the allowed URI schemes through the property <see cref="AllowedCssProperties"/>. All other URIs will be stripped.</item>
|
||||
/// <item>You can specify the HTML attributes that contain URIs (such as "src", "href" etc.) through the property <see cref="UriAttributes"/>.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// <![CDATA[
|
||||
/// var sanitizer = new HtmlSanitizer();
|
||||
/// var html = @"<script>alert('xss')</script><div onload=""alert('xss')"" style=""background-color: test"">Test<img src=""test.gif"" style=""background-image: url(javascript:alert('xss')); margin: 10px""></div>";
|
||||
/// var sanitized = sanitizer.Sanitize(html, "http://www.example.com");
|
||||
/// // -> "<div style="background-color: test">Test<img style="margin: 10px" src="http://www.example.com/test.gif"></div>"
|
||||
/// ]]>
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class HtmlSanitizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HtmlSanitizer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="allowedTags">The allowed tag names such as "a" and "div". When <c>null</c>, uses <see cref="DefaultAllowedTags"/></param>
|
||||
/// <param name="allowedSchemes">The allowed HTTP schemes such as "http" and "https". When <c>null</c>, uses <see cref="DefaultAllowedSchemes"/></param>
|
||||
/// <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>
|
||||
public HtmlSanitizer(IEnumerable<string> allowedTags = null, IEnumerable<string> allowedSchemes = null,
|
||||
IEnumerable<string> allowedAttributes = null, IEnumerable<string> uriAttributes = null, IEnumerable<string> allowedCssProperties = null)
|
||||
{
|
||||
AllowedTags = new HashSet<string>(allowedTags ?? DefaultAllowedTags, StringComparer.OrdinalIgnoreCase);
|
||||
AllowedSchemes = new HashSet<string>(allowedSchemes ?? DefaultAllowedSchemes, StringComparer.OrdinalIgnoreCase);
|
||||
AllowedAttributes = new HashSet<string>(allowedAttributes ?? DefaultAllowedAttributes, StringComparer.OrdinalIgnoreCase);
|
||||
UriAttributes = new HashSet<string>(uriAttributes ?? DefaultUriAttributes, StringComparer.OrdinalIgnoreCase);
|
||||
AllowedCssProperties = new HashSet<string>(allowedCssProperties ?? DefaultAllowedCssProperties, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed HTTP schemes such as "http" and "https".
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The allowed HTTP schemes.
|
||||
/// </value>
|
||||
public ISet<string> AllowedSchemes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default allowed URI schemes.
|
||||
/// </summary>
|
||||
public static readonly ISet<string> DefaultAllowedSchemes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "http", "https" };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed HTML tag names such as "a" and "div".
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The allowed tag names.
|
||||
/// </value>
|
||||
public ISet<string> AllowedTags { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default allowed HTML tag names.
|
||||
/// </summary>
|
||||
public static readonly ISet<string> DefaultAllowedTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
|
||||
// https://developer.mozilla.org/en/docs/Web/Guide/HTML/HTML5/HTML5_element_list
|
||||
"a", "abbr", "acronym", "address", "area", "b",
|
||||
@@ -111,24 +111,24 @@ namespace Ganss.XSS
|
||||
"datalist", "keygen", "output", "progress", "meter",
|
||||
// Interactive elements
|
||||
"details", "summary", "menuitem"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed HTML attributes such as "href" and "alt".
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The allowed HTML attributes.
|
||||
/// </value>
|
||||
public ISet<string> AllowedAttributes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Allow all HTML5 data attributes; the attributes prefixed with data-
|
||||
/// </summary>
|
||||
public bool AllowDataAttributes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default allowed HTML attributes.
|
||||
/// </summary>
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed HTML attributes such as "href" and "alt".
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The allowed HTML attributes.
|
||||
/// </value>
|
||||
public ISet<string> AllowedAttributes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Allow all HTML5 data attributes; the attributes prefixed with data-
|
||||
/// </summary>
|
||||
public bool AllowDataAttributes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default allowed HTML attributes.
|
||||
/// </summary>
|
||||
public static readonly ISet<string> DefaultAllowedAttributes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
|
||||
"abbr", "accept", "accept-charset", "accesskey",
|
||||
@@ -167,32 +167,32 @@ namespace Ganss.XSS
|
||||
"dropzone", // Global attribute
|
||||
"autocomplete", // <form>, <input>
|
||||
"autosave", // <input>
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTML attributes that can contain a URI such as "href".
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The URI attributes.
|
||||
/// </value>
|
||||
public ISet<string> UriAttributes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default URI attributes.
|
||||
/// </summary>
|
||||
public static readonly ISet<string> DefaultUriAttributes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "action", "background", "dynsrc", "href", "lowsrc", "src" };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed CSS properties such as "font" and "margin".
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The allowed CSS properties.
|
||||
/// </value>
|
||||
public ISet<string> AllowedCssProperties { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default allowed CSS properties.
|
||||
/// </summary>
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTML attributes that can contain a URI such as "href".
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The URI attributes.
|
||||
/// </value>
|
||||
public ISet<string> UriAttributes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default URI attributes.
|
||||
/// </summary>
|
||||
public static readonly ISet<string> DefaultUriAttributes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "action", "background", "dynsrc", "href", "lowsrc", "src" };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the allowed CSS properties such as "font" and "margin".
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The allowed CSS properties.
|
||||
/// </value>
|
||||
public ISet<string> AllowedCssProperties { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default allowed CSS properties.
|
||||
/// </summary>
|
||||
public static readonly ISet<string> DefaultAllowedCssProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
|
||||
// CSS 3 properties <http://www.w3.org/TR/CSS/#properties>
|
||||
"background", "background-attachment", "background-color",
|
||||
@@ -218,310 +218,310 @@ namespace Ganss.XSS
|
||||
"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" };
|
||||
|
||||
private Regex _disallowedCssPropertyValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a regex that must not match for legal CSS property values.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The regex.
|
||||
/// </value>
|
||||
public Regex DisallowCssPropertyValue
|
||||
{
|
||||
get { return _disallowedCssPropertyValue ?? DefaultDisallowedCssPropertyValue; }
|
||||
set { _disallowedCssPropertyValue = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before a tag is removed.
|
||||
/// </summary>
|
||||
public event EventHandler<RemovingTagEventArgs> RemovingTag;
|
||||
/// <summary>
|
||||
/// Occurs before an attribute is removed.
|
||||
/// </summary>
|
||||
public event EventHandler<RemovingAttributeEventArgs> RemovingAttribute;
|
||||
/// <summary>
|
||||
/// Occurs before a style is removed.
|
||||
/// </summary>
|
||||
public event EventHandler<RemovingStyleEventArgs> RemovingStyle;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="E:RemovingTag" /> event.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="RemovingTagEventArgs"/> instance containing the event data.</param>
|
||||
protected virtual void OnRemovingTag(RemovingTagEventArgs e)
|
||||
{
|
||||
if (RemovingTag != null) RemovingTag(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="E:RemovingAttribute" /> event.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="RemovingAttributeEventArgs"/> instance containing the event data.</param>
|
||||
protected virtual void OnRemovingAttribute(RemovingAttributeEventArgs e)
|
||||
{
|
||||
if (RemovingAttribute != null) RemovingAttribute(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="E:RemovingStyle" /> event.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="RemovingStyleEventArgs"/> instance containing the event data.</param>
|
||||
protected virtual void OnRemovingStyle(RemovingStyleEventArgs e)
|
||||
{
|
||||
if (RemovingStyle != null) RemovingStyle(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default regex for disallowed CSS property values.
|
||||
/// </summary>
|
||||
public static readonly Regex DefaultDisallowedCssPropertyValue = new Regex(@"[<>]", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes the specified HTML.
|
||||
/// </summary>
|
||||
/// <param name="html">The HTML to sanitize.</param>
|
||||
/// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
|
||||
/// <param name="outputFormatter">The CsQuery output formatter used to render the DOM. Using the default formatter if null.</param>
|
||||
/// <returns>The sanitized HTML.</returns>
|
||||
public string Sanitize(string html, string baseUrl = "", IOutputFormatter outputFormatter = null)
|
||||
{
|
||||
var dom = CQ.Create(html);
|
||||
|
||||
//remove non-whitelisted tags
|
||||
foreach (var tag in dom["*"].Where(t => !AllowedTags.Contains(t.NodeName)).ToList())
|
||||
{
|
||||
RemoveTag(tag);
|
||||
}
|
||||
|
||||
//cleanup attributes
|
||||
foreach (var tag in dom["*"])
|
||||
{
|
||||
//remove non-whitelisted attributes
|
||||
foreach (var attribute in tag.Attributes.Where(a => !IsAllowedAttribute(a)).ToList())
|
||||
{
|
||||
RemoveAttribute(tag, attribute);
|
||||
}
|
||||
|
||||
//sanitize URLs in URL-marked attributes
|
||||
foreach (var attribute in tag.Attributes.Where(a => UriAttributes.Contains(a.Key)).ToList())
|
||||
{
|
||||
var url = SanitizeUrl(attribute.Value, baseUrl);
|
||||
if (url == null)
|
||||
RemoveAttribute(tag, attribute);
|
||||
else
|
||||
tag.SetAttribute(attribute.Key, url);
|
||||
}
|
||||
|
||||
//sanitize the style attribute
|
||||
SanitizeStyle(tag.Style, baseUrl);
|
||||
|
||||
//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);
|
||||
else
|
||||
{
|
||||
//escape attribute value
|
||||
var val = attribute.Value.Replace("<", "<").Replace(">", ">");
|
||||
tag.SetAttribute(attribute.Key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (outputFormatter == null)
|
||||
outputFormatter = new FormatDefault(DomRenderingOptions.RemoveComments | DomRenderingOptions.QuoteAllAttributes, HtmlEncoders.Default);
|
||||
|
||||
var output = dom.Render(outputFormatter);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allowed Attribute
|
||||
/// </summary>
|
||||
/// <param name="attribute"></param>
|
||||
/// <returns></returns>
|
||||
private bool IsAllowedAttribute(KeyValuePair<string, string> attribute)
|
||||
{
|
||||
//test html5 data- attributes
|
||||
if (AllowDataAttributes && attribute.Key != null && attribute.Key.StartsWith("data-", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return AllowedAttributes.Contains(attribute.Key);
|
||||
}
|
||||
|
||||
// from http://genshi.edgewall.org/
|
||||
private static readonly Regex CssUnicodeEscapes = new Regex(@"\\([0-9a-fA-F]{1,6})\s?|\\([^\r\n\f0-9a-fA-F'""{};:()#*])", RegexOptions.Compiled);
|
||||
private static readonly Regex CssComments = new Regex(@"/\*.*?\*/", RegexOptions.Compiled);
|
||||
// IE6 <http://heideri.ch/jso/#80>
|
||||
private static readonly Regex CssExpression = new Regex(@"[eE\uFF25\uFF45][xX\uFF38\uFF58][pP\uFF30\uFF50][rR\u0280\uFF32\uFF52][eE\uFF25\uFF45][sS\uFF33\uFF53]{2}[iI\u026A\uFF29\uFF49][oO\uFF2F\uFF4F][nN\u0274\uFF2E\uFF4E]", RegexOptions.Compiled);
|
||||
private static readonly Regex CssUrl = new Regex(@"[Uu][Rr\u0280][Ll\u029F]\s*\(\s*['""]?\s*([^'"")]+)", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes the style.
|
||||
/// </summary>
|
||||
/// <param name="styles">The styles.</param>
|
||||
/// <param name="baseUrl">The base URL.</param>
|
||||
protected void SanitizeStyle(CSSStyleDeclaration styles, string baseUrl)
|
||||
{
|
||||
if (styles == null || !styles.Any()) return;
|
||||
|
||||
var removeStyles = new List<KeyValuePair<string, string>>();
|
||||
var setStyles = new Dictionary<string, string>();
|
||||
|
||||
foreach (var style in styles)
|
||||
{
|
||||
var key = DecodeCss(style.Key);
|
||||
var val = DecodeCss(style.Value);
|
||||
|
||||
if (!AllowedCssProperties.Contains(key) || CssExpression.IsMatch(val) || DisallowCssPropertyValue.IsMatch(val))
|
||||
removeStyles.Add(style);
|
||||
else
|
||||
{
|
||||
var urls = CssUrl.Matches(val);
|
||||
|
||||
if (urls.Count > 0)
|
||||
{
|
||||
if (urls.Cast<Match>().Any(m => GetSafeUri(m.Groups[1].Value) == null))
|
||||
removeStyles.Add(style);
|
||||
else
|
||||
{
|
||||
var s = CssUrl.Replace(val, m => "url(" + SanitizeUrl(m.Groups[1].Value, baseUrl));
|
||||
if (s != val)
|
||||
{
|
||||
if (key != style.Key) removeStyles.Add(style);
|
||||
setStyles[key] = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var style in removeStyles)
|
||||
{
|
||||
RemoveStyle(styles, style);
|
||||
}
|
||||
|
||||
foreach (var kvp in setStyles)
|
||||
{
|
||||
styles.SetStyle(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Decodes CSS unicode escapes and removes comments.
|
||||
/// </summary>
|
||||
/// <param name="css">The CSS string.</param>
|
||||
/// <returns>The decoded CSS string.</returns>
|
||||
protected static string DecodeCss(string css)
|
||||
{
|
||||
var r = CssUnicodeEscapes.Replace(css, m =>
|
||||
{
|
||||
if (m.Groups[1].Success)
|
||||
return ((char)int.Parse(m.Groups[1].Value, NumberStyles.HexNumber)).ToString();
|
||||
var t = m.Groups[2].Value;
|
||||
return t == "\\" ? @"\\" : t;
|
||||
});
|
||||
|
||||
r = CssComments.Replace(r, m => "");
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to create a safe <see cref="Uri"/> object from a string.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <returns>The <see cref="Uri"/> object or null if no safe <see cref="Uri"/> can be created.</returns>
|
||||
protected Uri GetSafeUri(string url)
|
||||
{
|
||||
Uri uri;
|
||||
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri)
|
||||
|| !uri.IsWellFormedOriginalString() && !IsWellFormedRelativeUri(uri)
|
||||
|| uri.IsAbsoluteUri && !AllowedSchemes.Contains(uri.Scheme, StringComparer.OrdinalIgnoreCase)
|
||||
|| !uri.IsAbsoluteUri && url.Contains(':'))
|
||||
return null;
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
private static readonly Uri _exampleUri = new Uri("http://www.example.com/");
|
||||
private static bool IsWellFormedRelativeUri(Uri uri)
|
||||
{
|
||||
if (uri.IsAbsoluteUri) return false;
|
||||
|
||||
Uri absoluteUri;
|
||||
if (!Uri.TryCreate(_exampleUri, uri, out absoluteUri)) return false;
|
||||
var wellFormed = absoluteUri.IsWellFormedOriginalString();
|
||||
return wellFormed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="baseUrl">The base URL relative URLs are resolved against (empty or null for no resolution).</param>
|
||||
/// <returns>The sanitized URL or null if no safe URL can be created.</returns>
|
||||
protected string SanitizeUrl(string url, string baseUrl)
|
||||
{
|
||||
var uri = GetSafeUri(url);
|
||||
|
||||
if (uri == null) return null;
|
||||
|
||||
if (!uri.IsAbsoluteUri && !string.IsNullOrEmpty(baseUrl))
|
||||
{
|
||||
// resolve relative uri
|
||||
Uri baseUri;
|
||||
if (Uri.TryCreate(baseUrl, UriKind.Absolute, out baseUri))
|
||||
uri = new Uri(baseUri, uri.ToString());
|
||||
else return null;
|
||||
}
|
||||
|
||||
return uri.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a tag from the document.
|
||||
/// </summary>
|
||||
/// <param name="tag">to be removed</param>
|
||||
private void RemoveTag(IDomObject tag)
|
||||
{
|
||||
var e = new RemovingTagEventArgs { Tag = tag };
|
||||
OnRemovingTag(e);
|
||||
if (!e.Cancel) tag.Remove();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an attribute from the document.
|
||||
/// </summary>
|
||||
/// <param name="tag">tag where the attribute to belongs</param>
|
||||
/// <param name="attribute">to be removed</param>
|
||||
private void RemoveAttribute(IDomObject tag, KeyValuePair<string, string> attribute)
|
||||
{
|
||||
var e = new RemovingAttributeEventArgs { Attribute = attribute };
|
||||
OnRemovingAttribute(e);
|
||||
if (!e.Cancel) tag.RemoveAttribute(attribute.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a style from the document.
|
||||
/// </summary>
|
||||
/// <param name="styles">collection where the style to belongs</param>
|
||||
/// <param name="style">to be removed</param>
|
||||
private void RemoveStyle(ICSSStyleDeclaration styles, KeyValuePair<string, string> style)
|
||||
{
|
||||
var e = new RemovingStyleEventArgs { Style = style };
|
||||
OnRemovingStyle(e);
|
||||
if (!e.Cancel) styles.RemoveStyle(style.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
"widows", "width", "word-spacing", "z-index" };
|
||||
|
||||
private Regex _disallowedCssPropertyValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a regex that must not match for legal CSS property values.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The regex.
|
||||
/// </value>
|
||||
public Regex DisallowCssPropertyValue
|
||||
{
|
||||
get { return _disallowedCssPropertyValue ?? DefaultDisallowedCssPropertyValue; }
|
||||
set { _disallowedCssPropertyValue = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before a tag is removed.
|
||||
/// </summary>
|
||||
public event EventHandler<RemovingTagEventArgs> RemovingTag;
|
||||
/// <summary>
|
||||
/// Occurs before an attribute is removed.
|
||||
/// </summary>
|
||||
public event EventHandler<RemovingAttributeEventArgs> RemovingAttribute;
|
||||
/// <summary>
|
||||
/// Occurs before a style is removed.
|
||||
/// </summary>
|
||||
public event EventHandler<RemovingStyleEventArgs> RemovingStyle;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="E:RemovingTag" /> event.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="RemovingTagEventArgs"/> instance containing the event data.</param>
|
||||
protected virtual void OnRemovingTag(RemovingTagEventArgs e)
|
||||
{
|
||||
if (RemovingTag != null) RemovingTag(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="E:RemovingAttribute" /> event.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="RemovingAttributeEventArgs"/> instance containing the event data.</param>
|
||||
protected virtual void OnRemovingAttribute(RemovingAttributeEventArgs e)
|
||||
{
|
||||
if (RemovingAttribute != null) RemovingAttribute(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="E:RemovingStyle" /> event.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="RemovingStyleEventArgs"/> instance containing the event data.</param>
|
||||
protected virtual void OnRemovingStyle(RemovingStyleEventArgs e)
|
||||
{
|
||||
if (RemovingStyle != null) RemovingStyle(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default regex for disallowed CSS property values.
|
||||
/// </summary>
|
||||
public static readonly Regex DefaultDisallowedCssPropertyValue = new Regex(@"[<>]", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes the specified HTML.
|
||||
/// </summary>
|
||||
/// <param name="html">The HTML to sanitize.</param>
|
||||
/// <param name="baseUrl">The base URL relative URLs are resolved against. No resolution if empty.</param>
|
||||
/// <param name="outputFormatter">The CsQuery output formatter used to render the DOM. Using the default formatter if null.</param>
|
||||
/// <returns>The sanitized HTML.</returns>
|
||||
public string Sanitize(string html, string baseUrl = "", IOutputFormatter outputFormatter = null)
|
||||
{
|
||||
var dom = CQ.Create(html);
|
||||
|
||||
//remove non-whitelisted tags
|
||||
foreach (var tag in dom["*"].Where(t => !AllowedTags.Contains(t.NodeName)).ToList())
|
||||
{
|
||||
RemoveTag(tag);
|
||||
}
|
||||
|
||||
//cleanup attributes
|
||||
foreach (var tag in dom["*"])
|
||||
{
|
||||
//remove non-whitelisted attributes
|
||||
foreach (var attribute in tag.Attributes.Where(a => !IsAllowedAttribute(a)).ToList())
|
||||
{
|
||||
RemoveAttribute(tag, attribute);
|
||||
}
|
||||
|
||||
//sanitize URLs in URL-marked attributes
|
||||
foreach (var attribute in tag.Attributes.Where(a => UriAttributes.Contains(a.Key)).ToList())
|
||||
{
|
||||
var url = SanitizeUrl(attribute.Value, baseUrl);
|
||||
if (url == null)
|
||||
RemoveAttribute(tag, attribute);
|
||||
else
|
||||
tag.SetAttribute(attribute.Key, url);
|
||||
}
|
||||
|
||||
//sanitize the style attribute
|
||||
SanitizeStyle(tag.Style, baseUrl);
|
||||
|
||||
//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);
|
||||
else
|
||||
{
|
||||
//escape attribute value
|
||||
var val = attribute.Value.Replace("<", "<").Replace(">", ">");
|
||||
tag.SetAttribute(attribute.Key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (outputFormatter == null)
|
||||
outputFormatter = new FormatDefault(DomRenderingOptions.RemoveComments | DomRenderingOptions.QuoteAllAttributes, HtmlEncoders.Default);
|
||||
|
||||
var output = dom.Render(outputFormatter);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allowed Attribute
|
||||
/// </summary>
|
||||
/// <param name="attribute"></param>
|
||||
/// <returns></returns>
|
||||
private bool IsAllowedAttribute(KeyValuePair<string, string> attribute)
|
||||
{
|
||||
//test html5 data- attributes
|
||||
if (AllowDataAttributes && attribute.Key != null && attribute.Key.StartsWith("data-", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return AllowedAttributes.Contains(attribute.Key);
|
||||
}
|
||||
|
||||
// from http://genshi.edgewall.org/
|
||||
private static readonly Regex CssUnicodeEscapes = new Regex(@"\\([0-9a-fA-F]{1,6})\s?|\\([^\r\n\f0-9a-fA-F'""{};:()#*])", RegexOptions.Compiled);
|
||||
private static readonly Regex CssComments = new Regex(@"/\*.*?\*/", RegexOptions.Compiled);
|
||||
// IE6 <http://heideri.ch/jso/#80>
|
||||
private static readonly Regex CssExpression = new Regex(@"[eE\uFF25\uFF45][xX\uFF38\uFF58][pP\uFF30\uFF50][rR\u0280\uFF32\uFF52][eE\uFF25\uFF45][sS\uFF33\uFF53]{2}[iI\u026A\uFF29\uFF49][oO\uFF2F\uFF4F][nN\u0274\uFF2E\uFF4E]", RegexOptions.Compiled);
|
||||
private static readonly Regex CssUrl = new Regex(@"[Uu][Rr\u0280][Ll\u029F]\s*\(\s*['""]?\s*([^'"")]+)", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes the style.
|
||||
/// </summary>
|
||||
/// <param name="styles">The styles.</param>
|
||||
/// <param name="baseUrl">The base URL.</param>
|
||||
protected void SanitizeStyle(CSSStyleDeclaration styles, string baseUrl)
|
||||
{
|
||||
if (styles == null || !styles.Any()) return;
|
||||
|
||||
var removeStyles = new List<KeyValuePair<string, string>>();
|
||||
var setStyles = new Dictionary<string, string>();
|
||||
|
||||
foreach (var style in styles)
|
||||
{
|
||||
var key = DecodeCss(style.Key);
|
||||
var val = DecodeCss(style.Value);
|
||||
|
||||
if (!AllowedCssProperties.Contains(key) || CssExpression.IsMatch(val) || DisallowCssPropertyValue.IsMatch(val))
|
||||
removeStyles.Add(style);
|
||||
else
|
||||
{
|
||||
var urls = CssUrl.Matches(val);
|
||||
|
||||
if (urls.Count > 0)
|
||||
{
|
||||
if (urls.Cast<Match>().Any(m => GetSafeUri(m.Groups[1].Value) == null))
|
||||
removeStyles.Add(style);
|
||||
else
|
||||
{
|
||||
var s = CssUrl.Replace(val, m => "url(" + SanitizeUrl(m.Groups[1].Value, baseUrl));
|
||||
if (s != val)
|
||||
{
|
||||
if (key != style.Key) removeStyles.Add(style);
|
||||
setStyles[key] = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var style in removeStyles)
|
||||
{
|
||||
RemoveStyle(styles, style);
|
||||
}
|
||||
|
||||
foreach (var kvp in setStyles)
|
||||
{
|
||||
styles.SetStyle(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Decodes CSS unicode escapes and removes comments.
|
||||
/// </summary>
|
||||
/// <param name="css">The CSS string.</param>
|
||||
/// <returns>The decoded CSS string.</returns>
|
||||
protected static string DecodeCss(string css)
|
||||
{
|
||||
var r = CssUnicodeEscapes.Replace(css, m =>
|
||||
{
|
||||
if (m.Groups[1].Success)
|
||||
return ((char)int.Parse(m.Groups[1].Value, NumberStyles.HexNumber)).ToString();
|
||||
var t = m.Groups[2].Value;
|
||||
return t == "\\" ? @"\\" : t;
|
||||
});
|
||||
|
||||
r = CssComments.Replace(r, m => "");
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to create a safe <see cref="Uri"/> object from a string.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <returns>The <see cref="Uri"/> object or null if no safe <see cref="Uri"/> can be created.</returns>
|
||||
protected Uri GetSafeUri(string url)
|
||||
{
|
||||
Uri uri;
|
||||
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri)
|
||||
|| !uri.IsWellFormedOriginalString() && !IsWellFormedRelativeUri(uri)
|
||||
|| uri.IsAbsoluteUri && !AllowedSchemes.Contains(uri.Scheme, StringComparer.OrdinalIgnoreCase)
|
||||
|| !uri.IsAbsoluteUri && url.Contains(':'))
|
||||
return null;
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
private static readonly Uri _exampleUri = new Uri("http://www.example.com/");
|
||||
private static bool IsWellFormedRelativeUri(Uri uri)
|
||||
{
|
||||
if (uri.IsAbsoluteUri) return false;
|
||||
|
||||
Uri absoluteUri;
|
||||
if (!Uri.TryCreate(_exampleUri, uri, out absoluteUri)) return false;
|
||||
var wellFormed = absoluteUri.IsWellFormedOriginalString();
|
||||
return wellFormed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="baseUrl">The base URL relative URLs are resolved against (empty or null for no resolution).</param>
|
||||
/// <returns>The sanitized URL or null if no safe URL can be created.</returns>
|
||||
protected string SanitizeUrl(string url, string baseUrl)
|
||||
{
|
||||
var uri = GetSafeUri(url);
|
||||
|
||||
if (uri == null) return null;
|
||||
|
||||
if (!uri.IsAbsoluteUri && !string.IsNullOrEmpty(baseUrl))
|
||||
{
|
||||
// resolve relative uri
|
||||
Uri baseUri;
|
||||
if (Uri.TryCreate(baseUrl, UriKind.Absolute, out baseUri))
|
||||
uri = new Uri(baseUri, uri.ToString());
|
||||
else return null;
|
||||
}
|
||||
|
||||
return uri.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a tag from the document.
|
||||
/// </summary>
|
||||
/// <param name="tag">to be removed</param>
|
||||
private void RemoveTag(IDomObject tag)
|
||||
{
|
||||
var e = new RemovingTagEventArgs { Tag = tag };
|
||||
OnRemovingTag(e);
|
||||
if (!e.Cancel) tag.Remove();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an attribute from the document.
|
||||
/// </summary>
|
||||
/// <param name="tag">tag where the attribute to belongs</param>
|
||||
/// <param name="attribute">to be removed</param>
|
||||
private void RemoveAttribute(IDomObject tag, KeyValuePair<string, string> attribute)
|
||||
{
|
||||
var e = new RemovingAttributeEventArgs { Attribute = attribute };
|
||||
OnRemovingAttribute(e);
|
||||
if (!e.Cancel) tag.RemoveAttribute(attribute.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a style from the document.
|
||||
/// </summary>
|
||||
/// <param name="styles">collection where the style to belongs</param>
|
||||
/// <param name="style">to be removed</param>
|
||||
private void RemoveStyle(ICSSStyleDeclaration styles, KeyValuePair<string, string> style)
|
||||
{
|
||||
var e = new RemovingStyleEventArgs { Style = style };
|
||||
OnRemovingStyle(e);
|
||||
if (!e.Cancel) styles.RemoveStyle(style.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{CCDB0C26-D683-4943-B5D8-AC07116461E5}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Ganss.XSS</RootNamespace>
|
||||
<AssemblyName>HtmlSanitizer</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<TargetFrameworkProfile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<RunCodeAnalysis>false</RunCodeAnalysis>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
<DocumentationFile>bin\Debug\HtmlSanitizer.XML</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="CsQuery">
|
||||
<HintPath>..\packages\CsQuery.1.3.4\lib\net40\CsQuery.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="EventArgs.cs" />
|
||||
<Compile Include="HtmlSanitizer.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{CCDB0C26-D683-4943-B5D8-AC07116461E5}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Ganss.XSS</RootNamespace>
|
||||
<AssemblyName>HtmlSanitizer</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<TargetFrameworkProfile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<RunCodeAnalysis>false</RunCodeAnalysis>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
<DocumentationFile>bin\Debug\HtmlSanitizer.XML</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="CsQuery">
|
||||
<HintPath>..\packages\CsQuery.1.3.4\lib\net40\CsQuery.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="EventArgs.cs" />
|
||||
<Compile Include="HtmlSanitizer.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
-->
|
||||
</Project>
|
||||
Reference in New Issue
Block a user