Initial commit

This commit is contained in:
Caner Patır
2018-05-04 15:39:39 +03:00
parent a3d68b2c0b
commit de57355d06
33 changed files with 13108 additions and 20 deletions

View File

@@ -3,7 +3,24 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27428.2043
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AntiSamy", "AntiSamy\AntiSamy.csproj", "{5F8A16B1-BA0E-44C5-89AE-E840E62D5425}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{665178DD-05A1-4CBF-AB33-868D6B845246}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{37861D31-8721-4536-94A0-B2977759643D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AntiSamy", "src\AntiSamy\AntiSamy.csproj", "{08092E21-FA74-4D3A-9FD8-D95A5C4C7A1A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AntiSamy.Tests", "test\AntiSamy.Tests\AntiSamy.Tests.csproj", "{0E6153A6-2B00-4290-AB1B-39D6D105B529}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{6AE9E6A9-4CC1-40C7-A858-5A2E6635E207}"
ProjectSection(SolutionItems) = preProject
appveyor.yml = appveyor.yml
build.cake = build.cake
build.ps1 = build.ps1
common.props = common.props
icon.png = icon.png
LICENCE = LICENCE
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,14 +28,22 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5F8A16B1-BA0E-44C5-89AE-E840E62D5425}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F8A16B1-BA0E-44C5-89AE-E840E62D5425}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F8A16B1-BA0E-44C5-89AE-E840E62D5425}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F8A16B1-BA0E-44C5-89AE-E840E62D5425}.Release|Any CPU.Build.0 = Release|Any CPU
{08092E21-FA74-4D3A-9FD8-D95A5C4C7A1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{08092E21-FA74-4D3A-9FD8-D95A5C4C7A1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08092E21-FA74-4D3A-9FD8-D95A5C4C7A1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08092E21-FA74-4D3A-9FD8-D95A5C4C7A1A}.Release|Any CPU.Build.0 = Release|Any CPU
{0E6153A6-2B00-4290-AB1B-39D6D105B529}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0E6153A6-2B00-4290-AB1B-39D6D105B529}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0E6153A6-2B00-4290-AB1B-39D6D105B529}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0E6153A6-2B00-4290-AB1B-39D6D105B529}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{08092E21-FA74-4D3A-9FD8-D95A5C4C7A1A} = {665178DD-05A1-4CBF-AB33-868D6B845246}
{0E6153A6-2B00-4290-AB1B-39D6D105B529} = {37861D31-8721-4536-94A0-B2977759643D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CED73542-BCAF-478E-AF4A-E6B36866D0AD}
EndGlobalSection

View File

@@ -1,7 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -1,8 +0,0 @@
using System;
namespace AntiSamy
{
public class Class1
{
}
}

21
LICENCE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 O<>uzhan Soykan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
AntiSamy .NET
========
A .net standard library for performing configurable cleansing of HTML coming from untrusted sources.
Another way of saying that could be: It's an API that helps you make sure that clients don't supply malicious cargo code in the HTML they supply for their profile, comments, etc.,
that get persisted on the server. The term "malicious code" in regards to web applications usually mean "JavaScript." Mostly, Cascading Stylesheets are only considered malicious
when they invoke the JavaScript. However, there are many situations where "normal" HTML and CSS can be used in a malicious manner.
How to Use
----------
First, add the dependency from Nuget
```powershall
install-package AntiSamy
```
```csharp
Policy antiSamyPolicy = Policy.FromFile("<your_antisamy_xml_file_path>")
AntiSamy antiSamy = new AntiSamy();
string yourDirtyInput = "<DIV><INPUT TYPE=\"IMAGE\" SRC=\"javascript:alert('XSS');\"></DIV>";
AntiSamyResult result = antiSamy.Scan(yourDirtyInput, antiSamyPolicy);
string cleanHtml = result.CleanHtml;
IEnumerable<string> errorMessages = result.ErrorMessages;
```
Referances
----------
* [OWASP AntiSamy Project - https://www.owasp.org/index.php/Category:OWASP_AntiSamy_Project](https://www.owasp.org/index.php/Category:OWASP_AntiSamy_Project)

10
appveyor.yml Normal file
View File

@@ -0,0 +1,10 @@
version: 1.0.{build}
configuration: Release
image: Visual Studio 2017
pull_requests:
do_not_increment_build_number: true
build_script:
- ps: .\build.ps1 -experimental
test: off

133
build.cake Normal file
View File

@@ -0,0 +1,133 @@
#tool "nuget:?package=xunit.runner.console&version=2.3.0-beta4-build3742"
#addin "nuget:?package=NuGet.Core"
#addin "nuget:?package=Cake.ExtendedNuGet"
//////////////////////////////////////////////////////////////////////
// ARGUMENTS
//////////////////////////////////////////////////////////////////////
var projectName = "Stove";
var solution = "./" + projectName + ".sln";
var target = Argument("target", "Default");
var configuration = Argument("configuration", "Release");
var toolpath = Argument("toolpath", @"tools");
var branch = Argument("branch", EnvironmentVariable("APPVEYOR_REPO_BRANCH"));
var nugetApiKey = EnvironmentVariable("nugetApiKey");
var isRelease = EnvironmentVariable("APPVEYOR_REPO_TAG") == "true";
var isPR = EnvironmentVariable("APPVEYOR_PULL_REQUEST_TITLE") != string.Empty;
var testProjects = new List<Tuple<string, string[]>>
{
new Tuple<string, string[]>("AntiSamy.Tests", new[] { "netcoreapp2.0" })
};
var nupkgPath = "nupkg";
var nupkgRegex = $"**/{projectName}*.nupkg";
var nugetPath = toolpath + "/nuget.exe";
var nugetQueryUrl = "https://www.nuget.org/api/v2/";
var nugetPushUrl = "https://www.nuget.org/api/v2/package";
var NUGET_PUSH_SETTINGS = new NuGetPushSettings
{
ToolPath = File(nugetPath),
Source = nugetPushUrl,
ApiKey = nugetApiKey
};
//////////////////////////////////////////////////////////////////////
// TASKS
//////////////////////////////////////////////////////////////////////
Task("Clean")
.Does(() =>
{
Information("Current Branch is:" + EnvironmentVariable("APPVEYOR_REPO_BRANCH"));
Information("Current Branch is:" + EnvironmentVariable("APPVEYOR_PULL_REQUEST_TITLE"));
Information($"IsRelase: {isRelease}");
CleanDirectories("./src/**/bin");
CleanDirectories("./src/**/obj");
CleanDirectory(nupkgPath);
});
Task("Restore-NuGet-Packages")
.IsDependentOn("Clean")
.Does(() =>
{
DotNetCoreRestore(solution);
});
Task("Build")
.IsDependentOn("Restore-NuGet-Packages")
.Does(() =>
{
MSBuild(solution, new MSBuildSettings(){Configuration = configuration}
.WithProperty("SourceLinkCreate","true"));
});
Task("Run-Unit-Tests")
.IsDependentOn("Build")
.Does(() =>
{
foreach (Tuple<string, string[]> testProject in testProjects)
{
foreach (string targetFramework in testProject.Item2)
{
if(targetFramework == "net461")
{
var testFile = GetFiles($"**/bin/{configuration}/{targetFramework}/{testProject.Item1}*.dll").First();
Information(testFile);
XUnit2(testFile.ToString(), new XUnit2Settings { });
}
else
{
var testProj = GetFiles($"./test/**/*{testProject.Item1}.csproj").First();
DotNetCoreTest(testProj.FullPath, new DotNetCoreTestSettings { Configuration = "Release", Framework = targetFramework });
}
}
}
});
Task("Pack")
.IsDependentOn("Run-Unit-Tests")
.Does(() =>
{
var nupkgFiles = GetFiles(nupkgRegex);
MoveFiles(nupkgFiles, nupkgPath);
});
Task("NugetPublish")
.IsDependentOn("Pack")
.WithCriteria(() => branch == "master" && !AppVeyor.Environment.PullRequest.IsPullRequest)
.Does(()=>
{
foreach(var nupkgFile in GetFiles(nupkgRegex))
{
if(!IsNuGetPublished(nupkgFile, nugetQueryUrl))
{
Information("Publishing... " + nupkgFile);
NuGetPush(nupkgFile, NUGET_PUSH_SETTINGS);
}
else
{
Information("Already published, skipping... " + nupkgFile);
}
}
});
//////////////////////////////////////////////////////////////////////
// TASK TARGETS
//////////////////////////////////////////////////////////////////////
Task("Default")
.IsDependentOn("Build")
.IsDependentOn("Run-Unit-Tests")
.IsDependentOn("Pack")
.IsDependentOn("NugetPublish");
//////////////////////////////////////////////////////////////////////
// EXECUTION
//////////////////////////////////////////////////////////////////////
RunTarget(target);

134
build.ps1 Normal file
View File

@@ -0,0 +1,134 @@
<#
.SYNOPSIS
This is a Powershell script to bootstrap a Cake build.
.DESCRIPTION
This Powershell script will download NuGet if missing, restore NuGet tools (including Cake)
and execute your Cake build script with the parameters you provide.
.PARAMETER Target
The build script target to run.
.PARAMETER Configuration
The build configuration to use.
.PARAMETER Verbosity
Specifies the amount of information to be displayed.
.PARAMETER WhatIf
Performs a dry run of the build script.
No tasks will be executed.
.PARAMETER ScriptArgs
Remaining arguments are added here.
.LINK
https://cakebuild.net
#>
[CmdletBinding()]
Param(
[string]$Target = "Default",
[ValidateSet("Release", "Debug")]
[string]$Configuration = "Release",
[ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
[string]$Verbosity = "Verbose",
[switch]$WhatIf,
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$ScriptArgs
)
$CakeVersion = "0.22.2"
$DotNetChannel = "LTS";
$DotNetVersion = "2.0.0";
$DotNetInstallerUri = "https://dot.net/v1/dotnet-install.ps1";
$NugetUrl = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
# Temporarily skip verification and opt-in to new in-proc NuGet
$ENV:CAKE_SETTINGS_SKIPVERIFICATION='true'
$ENV:CAKE_NUGET_USEINPROCESSCLIENT='true'
# Make sure tools folder exists
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
$ToolPath = Join-Path $PSScriptRoot "tools"
if (!(Test-Path $ToolPath)) {
Write-Verbose "Creating tools directory..."
New-Item -Path $ToolPath -Type directory | out-null
}
###########################################################################
# INSTALL .NET CORE CLI
###########################################################################
Function Remove-PathVariable([string]$VariableToRemove)
{
$path = [Environment]::GetEnvironmentVariable("PATH", "User")
if ($path -ne $null)
{
$newItems = $path.Split(';', [StringSplitOptions]::RemoveEmptyEntries) | Where-Object { "$($_)" -inotlike $VariableToRemove }
[Environment]::SetEnvironmentVariable("PATH", [System.String]::Join(';', $newItems), "User")
}
$path = [Environment]::GetEnvironmentVariable("PATH", "Process")
if ($path -ne $null)
{
$newItems = $path.Split(';', [StringSplitOptions]::RemoveEmptyEntries) | Where-Object { "$($_)" -inotlike $VariableToRemove }
[Environment]::SetEnvironmentVariable("PATH", [System.String]::Join(';', $newItems), "Process")
}
}
# Get .NET Core CLI path if installed.
$FoundDotNetCliVersion = $null;
if (Get-Command dotnet -ErrorAction SilentlyContinue) {
$FoundDotNetCliVersion = dotnet --version;
}
if($FoundDotNetCliVersion -ne $DotNetVersion) {
$InstallPath = Join-Path $PSScriptRoot ".dotnet"
if (!(Test-Path $InstallPath)) {
mkdir -Force $InstallPath | Out-Null;
}
(New-Object System.Net.WebClient).DownloadFile($DotNetInstallerUri, "$InstallPath\dotnet-install.ps1");
& $InstallPath\dotnet-install.ps1 -Channel $DotNetChannel -Version $DotNetVersion -InstallDir $InstallPath;
Remove-PathVariable "$InstallPath"
$env:PATH = "$InstallPath;$env:PATH"
}
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
$env:DOTNET_CLI_TELEMETRY_OPTOUT=1
###########################################################################
# INSTALL NUGET
###########################################################################
# Make sure nuget.exe exists.
$NugetPath = Join-Path $ToolPath "nuget.exe"
if (!(Test-Path $NugetPath)) {
Write-Host "Downloading NuGet.exe..."
(New-Object System.Net.WebClient).DownloadFile($NugetUrl, $NugetPath);
}
###########################################################################
# INSTALL CAKE
###########################################################################
# Make sure Cake has been installed.
$CakePath = Join-Path $ToolPath "Cake.$CakeVersion/Cake.exe"
if (!(Test-Path $CakePath)) {
Write-Host "Installing Cake..."
Invoke-Expression "&`"$NugetPath`" install Cake -Version $CakeVersion -OutputDirectory `"$ToolPath`"" | Out-Null;
if ($LASTEXITCODE -ne 0) {
Throw "An error occured while restoring Cake from NuGet."
}
}
###########################################################################
# RUN BUILD SCRIPT
###########################################################################
# Build the argument list.
$Arguments = @{
target=$Target;
configuration=$Configuration;
verbosity=$Verbosity;
dryrun=$WhatIf;
}.GetEnumerator() | %{"--{0}=`"{1}`"" -f $_.key, $_.value };
# Start Cake
Write-Host "Running build script..."
Invoke-Expression "& `"$CakePath`" `"build.cake`" $Arguments $ScriptArgs"
exit $LASTEXITCODE

28
common.props Normal file
View File

@@ -0,0 +1,28 @@
<Project>
<PropertyGroup>
<VersionPrefix>1.0.1</VersionPrefix>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<PackageIconUrl>https://raw.githubusercontent.com/canerpatir/AntiSamy.NET/master/icon.png</PackageIconUrl>
<PackageProjectUrl>https://github.com/canerpatir/AntiSamy.NET</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/canerpatir/AntiSamy.NET/blob/master/LICENCE</PackageLicenseUrl>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/canerpatir/AntiSamy.NET</RepositoryUrl>
<Authors>Oguzhan Soykan</Authors>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
<GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<GenerateAssemblyCopyrightAttribute>false</GenerateAssemblyCopyrightAttribute>
<GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SourceLink.Create.CommandLine" Version="2.7.6" PrivateAssets="All" />
</ItemGroup>
</Project>

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

26
src/AntiSamy/AntiSamy.cs Normal file
View File

@@ -0,0 +1,26 @@
namespace AntiSamy
{
public class AntiSamy
{
public string InputEncoding { get; } = AntiSamyDomScanner.DefaultEncodingAlgorithm;
public string OutputEncoding { get; } = AntiSamyDomScanner.DefaultEncodingAlgorithm;
public virtual AntiySamyResult Scan(string taintedHtml, string filename)
{
Policy policy = Policy.FromFile(filename);
var antiSamy = new AntiSamyDomScanner(policy);
return antiSamy.Scan(taintedHtml, InputEncoding, OutputEncoding);
}
public virtual AntiySamyResult Scan(string taintedHtml, Policy policy)
{
var antiSamy = new AntiSamyDomScanner(policy);
return antiSamy.Scan(taintedHtml, InputEncoding, OutputEncoding);
}
}
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="0.9.9.2" />
<PackageReference Include="HtmlAgilityPack" Version="1.8.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,373 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using AntiSamy.Model;
using HtmlAgilityPack;
namespace AntiSamy
{
public sealed class AntiSamyDomScanner
{
public const string DefaultEncodingAlgorithm = "UTF-8";
private readonly List<string> _errorMessages = new List<string>();
private readonly Policy _policy;
private int _num;
public AntiSamyDomScanner(Policy policy) => _policy = policy;
public AntiySamyResult Scan(string html, string inputEncoding, string outputEncoding)
{
if (html == null)
{
throw new ArgumentNullException(nameof(html));
}
//had problems with the &nbsp; getting double encoded, so this converts it to a literal space.
//this may need to be changed.
html = html.Replace("&nbsp;", char.Parse("\u00a0").ToString());
//We have to replace any invalid XML characters
html = StripNonValidXmlCharacters(html);
//holds the maximum input size for the incoming fragment
int maxInputSize = Policy.DefaultMaxInputSize;
//grab the size specified in the config file
try
{
maxInputSize = _policy.GetDirectiveAsInt("maxInputSize", int.MaxValue);
}
catch (FormatException fe)
{
Console.WriteLine("Format Exception: " + fe);
}
//ensure our input is less than the max
if (maxInputSize < html.Length)
{
throw new ScanException("File size [" + html.Length + "] is larger than maximum [" + maxInputSize + "]");
}
//grab start time (to be put in the result set along with end time)
DateTime start = DateTime.Now;
//fixes some weirdness in HTML agility
if (!HtmlNode.ElementsFlags.ContainsKey("iframe"))
{
HtmlNode.ElementsFlags.Add("iframe", HtmlElementFlag.Empty);
}
HtmlNode.ElementsFlags.Remove("form");
//Let's parse the incoming HTML
var doc = new HtmlDocument();
doc.LoadHtml(html);
//add closing tags
doc.OptionAutoCloseOnEnd = true;
//enforces XML rules, encodes big 5
doc.OptionOutputAsXml = true;
//loop through every node now, and enforce the rules held in the policy object
for (var i = 0; i < doc.DocumentNode.ChildNodes.Count; i++)
{
//grab current node
HtmlNode tmp = doc.DocumentNode.ChildNodes[i];
//this node can hold other nodes, so recursively validate
RecursiveValidateTag(tmp);
if (tmp.ParentNode == null)
{
i--;
}
}
string finalCleanHtml = doc.DocumentNode.InnerHtml;
return new AntiySamyResult(start, finalCleanHtml, _errorMessages);
}
private void RecursiveValidateTag(HtmlNode node)
{
int maxinputsize = _policy.GetDirectiveAsInt("maxInputSize", int.MaxValue);
_num++;
HtmlNode parentNode = node.ParentNode;
HtmlNode tmp = null;
string tagName = node.Name;
//check this out
//might not be robust enough
if (tagName.ToLower().Equals("#text")) // || tagName.ToLower().Equals("#comment"))
{
return;
}
DocumentTag tag = _policy.GetTag(tagName.ToLower());
if (tag == null || Consts.TagActions.FILTER.Equals(tag.Action))
{
var errBuff = new StringBuilder();
if (tagName.Trim().Equals(""))
{
errBuff.Append("An unprocessable ");
}
else
{
errBuff.Append("The <b>" + HtmlEntityEncoder.HtmlEntityEncode(tagName.ToLower()) + "</b> ");
}
errBuff.Append("tag has been filtered for security reasons. The contents of the tag will ");
errBuff.Append("remain in place.");
_errorMessages.Add(errBuff.ToString());
for (var i = 0; i < node.ChildNodes.Count; i++)
{
tmp = node.ChildNodes[i];
RecursiveValidateTag(tmp);
if (tmp.ParentNode == null)
{
i--;
}
}
PromoteChildren(node);
}
else if (Consts.TagActions.VALIDATE.Equals(tag.Action))
{
if ("style".Equals(tagName.ToLower()) && _policy.GetTag("style") != null)
{
ScanCss(node, parentNode, maxinputsize);
}
for (var currentAttributeIndex = 0; currentAttributeIndex < node.Attributes.Count; currentAttributeIndex++)
{
HtmlAttribute attribute = node.Attributes[currentAttributeIndex];
string name = attribute.Name;
string value = attribute.Value;
DocumentAttribute attr = tag.GetAttributeByName(name) ?? _policy.GetGlobalAttribute(name);
var isAttributeValid = false;
if ("style".Equals(name.ToLower()) && attr != null)
{
ScanCss(node, parentNode, maxinputsize);
}
if (attr != null)
{
//try to find out how robust this is - do I need to do this in a loop?
value = HtmlEntity.DeEntitize(value);
foreach (string allowedValue in attr.AllowedValues)
{
if (isAttributeValid)
{
break;
}
if (allowedValue != null && allowedValue.ToLower().Equals(value.ToLower()))
{
isAttributeValid = true;
}
}
foreach (string ptn in attr.AllowedRegExps)
{
if (isAttributeValid)
{
break;
}
string pattern = "^" + ptn + "$";
Match m = Regex.Match(value, pattern);
if (m.Success)
{
isAttributeValid = true;
}
}
if (!isAttributeValid)
{
string onInvalidAction = attr.OnInvalid;
var errBuff = new StringBuilder();
errBuff.Append("The <b>" + HtmlEntityEncoder.HtmlEntityEncode(tagName) + "</b> tag contained an attribute that we couldn't process. ");
errBuff.Append("The <b>" + HtmlEntityEncoder.HtmlEntityEncode(name) + "</b> attribute had a value of <u>" + HtmlEntityEncoder.HtmlEntityEncode(value) + "</u>. ");
errBuff.Append("This value could not be accepted for security reasons. We have chosen to ");
//Console.WriteLine(policy);
if (Consts.OnInvalidActions.REMOVE_TAG.Equals(onInvalidAction))
{
parentNode.RemoveChild(node);
errBuff.Append("remove the <b>" + HtmlEntityEncoder.HtmlEntityEncode(tagName) + "</b> tag and its contents in order to process this input. ");
}
else if (Consts.OnInvalidActions.FILTER_TAG.Equals(onInvalidAction))
{
for (var i = 0; i < node.ChildNodes.Count; i++)
{
tmp = node.ChildNodes[i];
RecursiveValidateTag(tmp);
if (tmp.ParentNode == null)
{
i--;
}
}
PromoteChildren(node);
errBuff.Append("filter the <b>" + HtmlEntityEncoder.HtmlEntityEncode(tagName) + "</b> tag and leave its contents in place so that we could process this input.");
}
else if (Consts.OnInvalidActions.REMOVE_ATTRIBUTE.Equals(onInvalidAction))
{
node.Attributes.Remove(attr.Name);
currentAttributeIndex--;
errBuff.Append("remove the <b>" + HtmlEntityEncoder.HtmlEntityEncode(name) + "</b> attribute from the tag and leave everything else in place so that we could process this input.");
}
_errorMessages.Add(errBuff.ToString());
if ("removeTag".Equals(onInvalidAction) || "filterTag".Equals(onInvalidAction))
{
return; // can't process any more if we remove/filter the tag
}
}
}
else
{
var errBuff = new StringBuilder();
errBuff.Append("The <b>" + HtmlEntityEncoder.HtmlEntityEncode(name));
errBuff.Append("</b> attribute of the <b>" + HtmlEntityEncoder.HtmlEntityEncode(tagName) + "</b> tag has been removed for security reasons. ");
errBuff.Append("This removal should not affect the display of the HTML submitted.");
_errorMessages.Add(errBuff.ToString());
node.Attributes.Remove(name);
currentAttributeIndex--;
}
}
for (var i = 0; i < node.ChildNodes.Count; i++)
{
tmp = node.ChildNodes[i];
RecursiveValidateTag(tmp);
if (tmp.ParentNode == null)
{
i--;
}
}
}
else if ("truncate".Equals(tag.Action) || Consts.TagActions.REMOVE.Equals(tag.Action))
{
Console.WriteLine("truncate");
HtmlAttributeCollection nnmap = node.Attributes;
while (nnmap.Count > 0)
{
var errBuff = new StringBuilder();
errBuff.Append("The <b>" + HtmlEntityEncoder.HtmlEntityEncode(nnmap[0].Name));
errBuff.Append("</b> attribute of the <b>" + HtmlEntityEncoder.HtmlEntityEncode(tagName) + "</b> tag has been removed for security reasons. ");
errBuff.Append("This removal should not affect the display of the HTML submitted.");
node.Attributes.Remove(nnmap[0].Name);
_errorMessages.Add(errBuff.ToString());
}
HtmlNodeCollection cList = node.ChildNodes;
var i = 0;
var j = 0;
int length = cList.Count;
while (i < length)
{
HtmlNode nodeToRemove = cList[j];
if (nodeToRemove.NodeType != HtmlNodeType.Text && nodeToRemove.NodeType != HtmlNodeType.Comment)
{
node.RemoveChild(nodeToRemove);
}
else
{
j++;
}
i++;
}
}
else
{
_errorMessages.Add("The <b>" + HtmlEntityEncoder.HtmlEntityEncode(tagName) + "</b> tag has been removed for security reasons.");
parentNode.RemoveChild(node);
}
}
private void ScanCss(HtmlNode node, HtmlNode parentNode, int maxinputsize)
{
var styleScanner = new CssScanner(_policy);
try
{
AntiySamyResult cssResult;
if (node.Attributes.Contains("style"))
{
cssResult = styleScanner.ScanStyleSheet(node.Attributes["style"].Value, maxinputsize);
node.Attributes["style"].Value = cssResult.CleanHtml;
}
else
{
cssResult = styleScanner.ScanStyleSheet(node.FirstChild.InnerHtml, maxinputsize);
node.FirstChild.InnerHtml = cssResult.CleanHtml;
}
_errorMessages.AddRange(cssResult.ErrorMessages);
}
catch (ParseException e)
{
parentNode.RemoveChild(node);
_errorMessages.Add($"Css could not be parsed. {e}");
}
}
private static void PromoteChildren(HtmlNode node)
{
HtmlNodeCollection nodeList = node.ChildNodes;
HtmlNode parent = node.ParentNode;
while (nodeList.Count > 0)
{
HtmlNode removeNode = node.RemoveChild(nodeList[0]);
parent.InsertBefore(removeNode, node);
}
parent.RemoveChild(node);
}
private static string StripNonValidXmlCharacters(string inRenamed)
{
var outRenamed = new StringBuilder(); // Used to hold the output.
if (inRenamed == null || "".Equals(inRenamed))
{
return ""; // vacancy test.
}
for (var i = 0; i < inRenamed.Length; i++)
{
char current = inRenamed[i]; // Used to reference the current character.
if (current == 0x9 || current == 0xA || current == 0xD || current >= 0x20 && current <= 0xD7FF || current >= 0xE000 && current <= 0xFFFD || current >= 0x10000 && current <= 0x10FFFF)
{
outRenamed.Append(current);
}
}
return outRenamed.ToString();
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
namespace AntiSamy
{
public class AntiySamyResult
{
public AntiySamyResult(DateTime startOfScan, string cleanHtml, IEnumerable<string> errorMessages)
{
Elapsed = DateTime.UtcNow - startOfScan;
CleanHtml = cleanHtml;
ErrorMessages = errorMessages;
}
public TimeSpan Elapsed { get; }
public string CleanHtml { get; }
public IEnumerable<string> ErrorMessages { get; }
}
}

24
src/AntiSamy/Consts.cs Normal file
View File

@@ -0,0 +1,24 @@
namespace AntiSamy
{
public class Consts
{
public const string ANY_NORMAL_WHITESPACES = "(\\s)*";
public const string OPEN_ATTRIBUTE = "(";
public const string ATTRIBUTE_DIVIDER = "|";
public const string CLOSE_ATTRIBUTE = ")";
public class OnInvalidActions
{
public const string REMOVE_TAG = "removeTag";
public const string REMOVE_ATTRIBUTE = "removeAttribute";
public const string FILTER_TAG = "filterTag";
}
public class TagActions
{
public const string FILTER = "filter"; // remove tags but keep content
public const string VALIDATE = "validate"; // keep content as long as it passes rules
public const string REMOVE = "remove"; // remove tag and contents
}
}
}

205
src/AntiSamy/CssScanner.cs Normal file
View File

@@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using AngleSharp.Dom.Css;
using AngleSharp.Extensions;
using AngleSharp.Parser.Css;
using AntiSamy.Model;
namespace AntiSamy
{
internal class CssScanner
{
private readonly Policy _policy;
private List<string> _errors = new List<string>();
public CssScanner(Policy policy) => _policy = policy ?? throw new ArgumentNullException(nameof(policy));
public AntiySamyResult ScanStyleSheet(string css, int maxinputsize)
{
DateTime start = DateTime.UtcNow;
_errors = new List<string>();
string cleanStyleSheet;
try
{
ICssStyleSheet styleSheet;
try
{
styleSheet = new CssParser(new CssParserOptions
{
IsIncludingUnknownDeclarations = true,
IsIncludingUnknownRules = true,
IsToleratingInvalidConstraints = true,
IsToleratingInvalidValues = true
}).ParseStylesheet(css);
}
catch (Exception ex)
{
throw new ParseException(ex.Message, ex);
}
cleanStyleSheet = ScanStyleSheet(styleSheet);
}
catch (ParseException)
{
throw;
}
catch (Exception exception)
{
throw new ScanException("An error occured while scanning css", exception);
}
return new AntiySamyResult(start, cleanStyleSheet, _errors);
}
private string ScanStyleSheet(ICssStyleSheet styleSheet)
{
for (var i = 0; i < styleSheet.Rules.Length;)
{
ICssRule rule = styleSheet.Rules[i];
if (!ScanStyleRule(rule))
styleSheet.RemoveAt(i);
else
i++;
}
return styleSheet.ToCss();
}
private bool ScanStyleRule(ICssRule rule)
{
if (rule is ICssStyleRule styleRule)
{
ScanStyleDeclaration(styleRule.Style);
}
else if (rule is ICssGroupingRule groupingRule)
{
foreach (ICssRule childRule in groupingRule.Rules)
{
ScanStyleRule(childRule);
}
}
else if (rule is ICssPageRule pageRule)
{
ScanStyleDeclaration(pageRule.Style);
}
else if (rule is ICssKeyframesRule keyFramesRule)
{
foreach (ICssKeyframeRule childRule in keyFramesRule.Rules.OfType<ICssKeyframeRule>().ToList())
{
ScanStyleRule(childRule);
}
}
else if (rule is ICssKeyframeRule keyFrameRule)
{
ScanStyleDeclaration(keyFrameRule.Style);
}
else if (rule is ICssImportRule importRule)
{
//Dont allow import rules for now
return false;
}
return true;
}
private void ScanStyleDeclaration(ICssStyleDeclaration styles)
{
var removingProperties = new List<Tuple<ICssProperty, string>>();
var cssUrlTest = new Regex(@"[Uu][Rr\u0280][Ll\u029F]\s*\(\s*(['""]?)\s*([^'"")\s]+)\s*(['""]?)\s*", RegexOptions.Compiled);
var dangerousCssExpressionTest = 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);
foreach (ICssProperty cssProperty in styles)
{
string key = DecodeCss(cssProperty.Name);
string value = DecodeCss(cssProperty.Value);
CssProperty allowedCssProperty = _policy.GetCssProperty(key);
if (allowedCssProperty == null)
{
removingProperties.Add(new Tuple<ICssProperty, string>(cssProperty, $"Css property \"{key}\" is not allowed"));
continue;
}
if (dangerousCssExpressionTest.IsMatch(value))
{
removingProperties.Add(new Tuple<ICssProperty, string>(cssProperty, $"\"{value}\" is invalid css expression"));
continue;
}
ValidateValue(allowedCssProperty, cssProperty, value, removingProperties);
MatchCollection urls = cssUrlTest.Matches(value);
if (urls.Count > 0)
{
var schemeRegex = new Regex(@"^\s*([^\/#]*?)(?:\:|&#0*58|&#x0*3a)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
if (!urls.Cast<Match>().All(u => schemeRegex.IsMatch(u.Value)))
{
removingProperties.Add(new Tuple<ICssProperty, string>(cssProperty, "Illegal url detected."));
}
}
}
foreach (Tuple<ICssProperty, string> style in removingProperties)
{
styles.RemoveProperty(style.Item1.Name);
_errors.Add(style.Item2);
}
}
private void ValidateValue(CssProperty allowedCssProperty, ICssProperty cssProperty, string value, List<Tuple<ICssProperty, string>> removeStyles)
{
if (!allowedCssProperty.AllowedLiterals.Any(lit => lit.Equals(value, StringComparison.OrdinalIgnoreCase)))
{
removeStyles.Add(new Tuple<ICssProperty, string>(cssProperty, $"\"{value}\" is not allowed literal"));
return;
}
if (!allowedCssProperty.AllowedRegExps.Any(regex => new Regex(regex).IsMatch(value)))
{
removeStyles.Add(new Tuple<ICssProperty, string>(cssProperty, $"\"{value}\" is not allowed literal by regex"));
return;
}
foreach (string shortHandRef in allowedCssProperty.ShorthandRefs)
{
CssProperty shorthand = _policy.GetCssProperty(shortHandRef);
if (shorthand != null)
{
ValidateValue(shorthand, cssProperty, value, removeStyles);
}
}
}
private static string DecodeCss(string css)
{
var cssComments = new Regex(@"/\*.*?\*/", RegexOptions.Compiled);
var cssUnicodeEscapes = new Regex(@"\\([0-9a-fA-F]{1,6})\s?|\\([^\r\n\f0-9a-fA-F'""{};:()#*])", RegexOptions.Compiled);
string r = cssUnicodeEscapes.Replace(css, m =>
{
if (m.Groups[1].Success)
{
return ((char)int.Parse(m.Groups[1].Value, NumberStyles.HexNumber)).ToString();
}
string t = m.Groups[2].Value;
return t == "\\" ? @"\\" : t;
});
r = cssComments.Replace(r, m => "");
return r;
}
}
}

View File

@@ -0,0 +1,49 @@
using System.Text;
namespace AntiSamy
{
internal class HtmlEntityEncoder
{
public static string HtmlEntityEncode(string value)
{
var sb = new StringBuilder();
if (value == null)
{
return null;
}
for (var i = 0; i < value.Length; i++)
{
char ch = value[i];
switch (ch)
{
case '&':
sb.Append("&amp;");
break;
case '<':
sb.Append("&lt;");
break;
case '>':
sb.Append("&gt;");
break;
default:
if (char.IsWhiteSpace(ch))
{
sb.Append(ch);
}
else if (char.IsLetterOrDigit(ch))
{
sb.Append(ch);
}
else if (ch >= 20 && ch <= 126)
{
sb.Append("&#" + (int)ch + ";");
}
break;
}
}
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace AntiSamy.Model
{
public class CssProperty
{
public CssProperty(string name, IEnumerable<string> allowedRegexps, IEnumerable<string> allowedLiterals, IEnumerable<string> shortHandRefs, string description, string onInvalid)
{
Description = description;
Name = name;
OnInvalid = onInvalid;
AllowedRegExps = allowedRegexps ?? new List<string>();
AllowedLiterals = allowedLiterals ?? new List<string>();
ShorthandRefs = shortHandRefs ?? new List<string>();
}
public string Description { get; }
public string Name { get; }
public string OnInvalid { get; }
public IEnumerable<string> AllowedLiterals { get; }
public IEnumerable<string> AllowedRegExps { get; }
public IEnumerable<string> ShorthandRefs { get; }
}
}

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace AntiSamy.Model
{
public class DocumentAttribute : ICloneable
{
public string Name { get; }
public string OnInvalid { get; internal set; }
public string Description { get; internal set; }
public List<string> AllowedValues { get; } = new List<string>();
public List<string> AllowedRegExps { get; } = new List<string>();
public DocumentAttribute(string name, List<string> allowedRegexps, List<string> allowedValues, string onInvalidStr, string description)
{
this.Name = name;
this.AllowedRegExps = allowedRegexps;
this.AllowedValues = allowedValues;
this.OnInvalid = onInvalidStr;
this.Description = description;
}
public bool MatchesAllowedExpression(string value)
{
string input = value.ToLower();
foreach (string patternStr in AllowedRegExps)
{
if (patternStr == null)
{
continue;
}
var pattern = new Regex(patternStr);
if (pattern.Matches(input).Count > 0)
{
return true;
}
}
return false;
}
public DocumentAttribute Mutate(string onInvalid, string description)
{
return new DocumentAttribute(Name,
AllowedRegExps.ToList(),
AllowedValues.ToList(),
!string.IsNullOrEmpty(onInvalid) ? onInvalid : OnInvalid,
!string.IsNullOrEmpty(description) ? description : Description);
}
public string MatcherRegEx(bool hasNext)
{
// <p (id=#([0-9.*{6})|sdf).*>
var regExp = new StringBuilder();
regExp.Append(Name)
.Append(Consts.ANY_NORMAL_WHITESPACES)
.Append("=")
.Append(Consts.ANY_NORMAL_WHITESPACES)
.Append("\"")
.Append(Consts.OPEN_ATTRIBUTE);
bool hasRegExps = AllowedRegExps.Any();
if (AllowedRegExps.Count() + AllowedValues.Count() > 0)
{
foreach (string allowedValue in AllowedValues)
{
regExp.Append(DocumentTag.EscapeRegularExpressionCharacters(allowedValue));
if (AllowedValues.Last() != allowedValue || hasRegExps)
{
regExp.Append(Consts.ATTRIBUTE_DIVIDER);
}
}
foreach (string allowedRegExp in AllowedRegExps)
{
regExp.Append(allowedRegExp);
if (AllowedRegExps.Last() != allowedRegExp)
{
regExp.Append(Consts.ATTRIBUTE_DIVIDER);
}
}
if (this.AllowedRegExps.Count() + this.AllowedValues.Count() > 0)
{
regExp.Append(Consts.CLOSE_ATTRIBUTE);
}
regExp.Append("\"" + Consts.ANY_NORMAL_WHITESPACES);
if (hasNext)
{
regExp.Append(Consts.ATTRIBUTE_DIVIDER);
}
}
return regExp.ToString();
}
public object Clone() => new DocumentAttribute(Name, AllowedRegExps, AllowedValues, OnInvalid, Description);
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace AntiSamy.Model
{
public class DocumentTag
{
private static readonly string OPEN_TAG_ATTRIBUTES = Consts.ANY_NORMAL_WHITESPACES + Consts.OPEN_ATTRIBUTE;
private static readonly string CLOSE_TAG_ATTRIBUTES = ")*";
private static readonly string REGEXP_CHARACTERS = "\\(){}.*?$^-+";
private readonly Dictionary<string, DocumentAttribute> _allowedAttributes = new Dictionary<string, DocumentAttribute>();
public DocumentTag(string name, string action)
{
Name = name;
Action = action;
}
public string Name { get; }
public IReadOnlyDictionary<string, DocumentAttribute> AllowedAttributes => _allowedAttributes;
public string Action { get; }
public void AddAllowedAttribute(DocumentAttribute attr)
{
_allowedAttributes[attr.Name] = attr;
}
public bool IsAction(string action) => action.Equals(Action);
public string GetRegularExpression()
{
if (_allowedAttributes.Count == 0)
{
return "^<" + Name + ">$";
}
var regExp = new StringBuilder("<" + Consts.ANY_NORMAL_WHITESPACES + Name + OPEN_TAG_ATTRIBUTES);
List<DocumentAttribute> values = _allowedAttributes.Values.OrderBy(a => a.Name).ToList();
foreach (DocumentAttribute attr in values)
{
regExp.Append(attr.MatcherRegEx(values.Last() != attr));
}
regExp.Append(CLOSE_TAG_ATTRIBUTES + Consts.ANY_NORMAL_WHITESPACES + ">");
return regExp.ToString();
}
public static string EscapeRegularExpressionCharacters(string allowedValue)
{
string toReturn = allowedValue;
if (toReturn == null)
{
return null;
}
for (var i = 0; i < REGEXP_CHARACTERS.Length; i++)
{
toReturn = toReturn.Replace("\\" + Convert.ToString(REGEXP_CHARACTERS.ElementAt(i)), "\\" + REGEXP_CHARACTERS.ElementAt(i));
}
return toReturn;
}
public DocumentAttribute GetAttributeByName(string name) => _allowedAttributes.TryGetValue(name, out DocumentAttribute val) ? val : null;
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace AntiSamy
{
public class ParseException : Exception
{
public ParseException(string message)
: base(message)
{
}
public ParseException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

431
src/AntiSamy/Policy.cs Normal file
View File

@@ -0,0 +1,431 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using AntiSamy.Model;
namespace AntiSamy
{
public class Policy
{
public const string DefaultOninvalid = "removeAttribute";
public const int DefaultMaxInputSize = 100000;
private const char RegexpBegin = '^';
private const char RegexpEnd = '$';
private readonly bool _fromXml;
private readonly string _xml;
private Policy(FileInfo file)
: this(file.FullName, false)
{
}
private Policy(string xml, bool fromXml)
{
_xml = xml ?? throw new ArgumentNullException(nameof(xml));
_fromXml = fromXml;
}
public IReadOnlyDictionary<string, DocumentAttribute> CommonAttributes { get; private set; } = new Dictionary<string, DocumentAttribute>();
public IReadOnlyDictionary<string, string> CommonRegularExpressions { get; private set; } = new Dictionary<string, string>();
public IReadOnlyDictionary<string, CssProperty> CssRules { get; private set; } = new Dictionary<string, CssProperty>();
public IReadOnlyDictionary<string, DocumentTag> TagRules { get; private set; } = new Dictionary<string, DocumentTag>();
public IReadOnlyDictionary<string, string> Directives { get; private set; } = new Dictionary<string, string>();
public IReadOnlyDictionary<string, DocumentAttribute> GlobalTagAttributes { get; private set; } = new Dictionary<string, DocumentAttribute>();
private void Load()
{
try
{
var doc = new XmlDocument();
if (_fromXml)
{
doc.LoadXml(_xml);
}
else //from filename
{
doc.Load(_xml);
}
XmlNode commonRegularExpressionListNode = doc.GetElementsByTagName("common-regexps")[0];
CommonRegularExpressions = ParseCommonRegExps(commonRegularExpressionListNode);
XmlNode directiveListNode = doc.GetElementsByTagName("directives")[0];
Directives = ParseDirectives(directiveListNode);
XmlNode commonAttributeListNode = doc.GetElementsByTagName("common-attributes")[0];
CommonAttributes = ParseCommonAttributes(commonAttributeListNode);
XmlNode globalAttributesListNode = doc.GetElementsByTagName("global-tag-attributes")[0];
GlobalTagAttributes = ParseGlobalAttributes(globalAttributesListNode);
XmlNode tagListNode = doc.GetElementsByTagName("tag-rules")[0];
TagRules = ParseTagRules(tagListNode);
XmlNode cssListNode = doc.GetElementsByTagName("css-rules")[0];
CssRules = ParseCssRules(cssListNode);
}
catch (Exception ex)
{
throw new PolicyException("Policy parsing error", ex);
}
}
public string GetRegularExpression(string name)
{
if (name == null || !CommonRegularExpressions.ContainsKey(name))
{
return null;
}
return CommonRegularExpressions[name];
}
public DocumentAttribute GetGlobalAttribute(string name) => GlobalTagAttributes.TryGetValue(name, out DocumentAttribute val) ? val : null;
public DocumentTag GetTag(string tagName) => TagRules.TryGetValue(tagName, out DocumentTag value) ? value : null;
public CssProperty GetCssProperty(string propertyName) => CssRules.TryGetValue(propertyName, out CssProperty value) ? value : null;
public int GetDirectiveAsInt(string name, int defaultval) => GetDirective(name) != null ? int.Parse(GetDirective(name)) : defaultval;
public string GetDirective(string name) => Directives.TryGetValue(name, out string value) ? value : null;
#region Parsing methods
private Dictionary<string, string> ParseDirectives(XmlNode directiveListNode)
{
XmlNodeList directiveNodes = directiveListNode.SelectNodes("directive");
var directives = new Dictionary<string, string>();
string name = "", value = "";
foreach (XmlNode node in directiveNodes)
{
if (node.NodeType == XmlNodeType.Element)
{
name = node.Attributes[0].Value;
value = node.Attributes[1].Value;
if (!directives.ContainsKey(name))
{
directives.Add(name, value);
}
}
}
return directives;
}
private Dictionary<string, DocumentAttribute> ParseGlobalAttributes(XmlNode globalAttributeListNode)
{
XmlNodeList globalAttributeNodes = globalAttributeListNode.SelectNodes("attribute");
var globalAttributes = new Dictionary<string, DocumentAttribute>(StringComparer.InvariantCultureIgnoreCase);
//string _value = "";
foreach (XmlNode node in globalAttributeNodes)
{
if (node.NodeType == XmlNodeType.Element)
{
string name = node.Attributes[0].Value;
DocumentAttribute toAdd = CommonAttributes[name];
if (toAdd != null)
{
globalAttributes.Add(name, toAdd);
}
else
{
throw new PolicyException("Global attribute '" + name + "' was not defined in <common-attributes>");
}
//if (!globalAttributes.ContainsKey(_name))
// globalAttributes.Add(_name, new AntiSamyPattern(_name, _value));
}
}
return globalAttributes;
}
private Dictionary<string, string> ParseCommonRegExps(XmlNode commonRegularExpressionListNode)
{
XmlNodeList list = commonRegularExpressionListNode.SelectNodes("regexp");
var commonRegularExpressions = new Dictionary<string, string>();
foreach (XmlNode node in list)
{
if (node.NodeType == XmlNodeType.Element)
{
string name = node.Attributes[0].Value;
string value = node.Attributes[1].Value;
if (!commonRegularExpressions.ContainsKey(name))
{
commonRegularExpressions.Add(name, value);
}
}
}
return commonRegularExpressions;
}
private Dictionary<string, DocumentAttribute> ParseCommonAttributes(XmlNode commonAttributeListNode)
{
XmlNodeList commonAttributeNodes = commonAttributeListNode.SelectNodes("attribute");
var commonAttributes = new Dictionary<string, DocumentAttribute>(StringComparer.InvariantCultureIgnoreCase);
foreach (XmlNode node in commonAttributeNodes)
{
if (node.NodeType == XmlNodeType.Element)
{
var allowedRegExp = new List<string>();
XmlNodeList regExpListNode = node.SelectNodes("regexp-list");
if (regExpListNode != null && regExpListNode.Count > 0)
{
XmlNodeList regExpList = regExpListNode[0].SelectNodes("regexp");
foreach (XmlNode regExpNode in regExpList)
{
string regExpName = regExpNode.Attributes["name"]?.Value;
string value = regExpNode.Attributes["value"]?.Value;
//TODO: java version uses "Pattern" class to hold regular expressions. I'm storing them as strings below
//find out if I need an equiv to pattern
if (!string.IsNullOrEmpty(regExpName))
{
allowedRegExp.Add(GetRegularExpression(regExpName));
}
else
{
allowedRegExp.Add(RegexpBegin + value + RegexpEnd);
}
}
}
var allowedValues = new List<string>();
XmlNode literalListNode = node.SelectNodes("literal-list")[0];
if (literalListNode != null)
{
XmlNodeList literalNodes = literalListNode.SelectNodes("literal");
foreach (XmlNode literalNode in literalNodes)
{
string value = literalNode.Attributes["value"]?.Value;
if (!string.IsNullOrEmpty(value))
{
allowedValues.Add(value);
}
else if (literalNode.Value != null)
{
allowedValues.Add(literalNode.Value);
}
}
}
string onInvalid = node.Attributes["onInvalid"]?.Value;
string name = node.Attributes["name"]?.Value;
var attribute = new DocumentAttribute(name,
allowedRegExp,
allowedValues,
!string.IsNullOrEmpty(onInvalid) ? onInvalid : DefaultOninvalid,
node.Attributes["description"]?.Value);
commonAttributes.Add(name, attribute);
}
}
return commonAttributes;
}
private Dictionary<string, DocumentTag> ParseTagRules(XmlNode tagAttributeListNode)
{
var tags = new Dictionary<string, DocumentTag>(StringComparer.InvariantCultureIgnoreCase);
XmlNodeList tagList = tagAttributeListNode.SelectNodes("tag");
foreach (XmlNode tagNode in tagList)
{
if (tagNode.NodeType == XmlNodeType.Element)
{
string name = tagNode.Attributes["name"]?.Value;
string action = tagNode.Attributes["action"]?.Value;
var tag = new DocumentTag(name, action);
XmlNodeList attributeList = tagNode.SelectNodes("attribute");
foreach (XmlNode attributeNode in attributeList)
{
if (!attributeNode.HasChildNodes)
{
CommonAttributes.TryGetValue(attributeNode.Attributes["name"].Value, out DocumentAttribute attribute);
if (attribute != null)
{
string onInvalid = attributeNode.Attributes["onInvalid"]?.Value;
string description = attributeNode.Attributes["description"]?.Value;
if (!string.IsNullOrEmpty(onInvalid))
{
attribute.OnInvalid = onInvalid;
}
if (!string.IsNullOrEmpty(description))
{
attribute.Description = description;
}
tag.AddAllowedAttribute((DocumentAttribute)attribute.Clone());
}
}
else
{
var allowedRegExps = new List<string>();
XmlNode regExpListNode = attributeNode.SelectNodes("regexp-list")[0];
if (regExpListNode != null)
{
XmlNodeList regExpList = regExpListNode.SelectNodes("regexp");
foreach (XmlNode regExpNode in regExpList)
{
string regExpName = regExpNode.Attributes["name"]?.Value;
string value = regExpNode.Attributes["value"]?.Value;
if (!string.IsNullOrEmpty(regExpName))
{
//AntiSamyPattern pattern = getRegularExpression(regExpName);
string pattern = GetRegularExpression(regExpName);
if (pattern != null)
{
allowedRegExps.Add(pattern);
}
//attribute.addAllowedRegExp(pattern.Pattern);
else
{
throw new PolicyException("Regular expression '" + regExpName + "' was referenced as a common regexp in definition of '" + tag.Name + "', but does not exist in <common-regexp>");
}
}
else if (!string.IsNullOrEmpty(value))
{
allowedRegExps.Add(RegexpBegin + value + RegexpEnd);
}
}
}
var allowedValues = new List<string>();
XmlNode literalListNode = attributeNode.SelectNodes("literal-list")[0];
if (literalListNode != null)
{
XmlNodeList literalNodes = literalListNode.SelectNodes("literal");
foreach (XmlNode literalNode in literalNodes)
{
string value = literalNode.Attributes["value"]?.Value;
if (!string.IsNullOrEmpty(value))
{
allowedValues.Add(value);
}
else if (literalNode.Value != null)
{
allowedValues.Add(literalNode.Value);
}
}
}
/* Custom attribute for this tag */
var attribute = new DocumentAttribute(attributeNode.Attributes["name"].Value,
allowedRegExps,
allowedValues,
attributeNode.Attributes["onInvalid"]?.Value,
attributeNode.Attributes["description"]?.Value);
tag.AddAllowedAttribute(attribute);
}
}
tags.Add(name, tag);
}
}
return tags;
}
private Dictionary<string, CssProperty> ParseCssRules(XmlNode cssNodeList)
{
var properties = new Dictionary<string, CssProperty>(StringComparer.InvariantCultureIgnoreCase);
XmlNodeList propertyNodes = cssNodeList.SelectNodes("property");
/*
* Loop through the list of attributes and add them to the collection.
*/
foreach (XmlNode ele in propertyNodes)
{
string name = ele.Attributes["name"]?.Value;
string description = ele.Attributes["description"]?.Value;
string oninvalid = ele.Attributes["onInvalid"]?.Value;
var allowedRegExps = new List<string>();
XmlNode regExpListNode = ele.SelectNodes("regexp-list")[0];
if (regExpListNode != null)
{
XmlNodeList regExpList = regExpListNode.SelectNodes("regexp");
foreach (XmlNode regExpNode in regExpList)
{
string regExpName = regExpNode.Attributes["name"]?.Value;
string value = regExpNode.Attributes["value"]?.Value;
string pattern = GetRegularExpression(regExpName);
if (pattern != null)
{
allowedRegExps.Add(pattern);
}
else if (value != null)
{
allowedRegExps.Add(RegexpBegin + value + RegexpEnd);
}
else
{
throw new PolicyException("Regular expression '" + regExpName + "' was referenced as a common regexp in definition of '" + name + "', but does not exist in <common-regexp>");
}
}
}
var allowedLiterals = new List<string>();
XmlNode literalListNode = ele.SelectNodes("literal-list")[0];
if (literalListNode != null)
{
XmlNodeList literalList = literalListNode.SelectNodes("literal");
foreach (XmlNode literalNode in literalList)
{
allowedLiterals.Add(literalNode.Attributes["value"].Value);
}
}
var shorthandRefs = new List<string>();
XmlNode shorthandListNode = ele.SelectNodes("shorthand-list")[0];
if (shorthandListNode != null)
{
XmlNodeList shorthandList = shorthandListNode.SelectNodes("shorthand");
foreach (XmlNode shorthandNode in shorthandList)
{
shorthandRefs.Add(shorthandNode.Attributes["name"].Value);
}
}
properties.Add(name, new CssProperty(name,
allowedRegExps,
allowedLiterals,
shorthandRefs,
description,
!string.IsNullOrEmpty(oninvalid) ? oninvalid : DefaultOninvalid));
}
return properties;
}
#endregion
#region Factory methods
public static Policy Load(string filename, bool fromXml)
{
var policy = new Policy(filename, fromXml);
policy.Load();
return policy;
}
public static Policy FromFile(string filename) => Load(filename, false);
public static Policy FromFile(FileInfo fileInfo) => FromFile(fileInfo.FullName);
public static Policy FromXml(string xml) => Load(xml, true);
#endregion
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace AntiSamy
{
public class PolicyException : Exception
{
public PolicyException(string message)
: base(message)
{
}
public PolicyException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace AntiSamy
{
public class ScanException : Exception
{
public ScanException(string message, Exception innerException)
: base(message, innerException)
{
}
public ScanException(string message)
: base(message)
{
}
}
}

View File

@@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="resources\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\AntiSamy\AntiSamy.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="resources\antisamy-anythinggoes.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="resources\antisamy-ebay.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="resources\antisamy-myspace.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="resources\antisamy-slashdot.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="resources\antisamy-tinymce.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="resources\antisamy.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="resources\antisamy.xsd">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,183 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FluentAssertions;
using Xunit;
namespace AntiSamy.Tests
{
public class AntiSamyTests
{
private static readonly String[] BASE64_BAD_XML_STRINGS = new String[]{
// first string is
// "<a - href=\"http://www.owasp.org\">click here</a>"
"PGEgLSBocmVmPSJodHRwOi8vd3d3Lm93YXNwLm9yZyI+Y2xpY2sgaGVyZTwvYT4=",
// the rest are randomly generated 300 byte sequences which generate
// parser errors, turned into Strings
"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",
};
private AntiSamy _sut = new AntiSamy();
private Policy GetTestPolicy()
{
var currentDir = Directory.GetCurrentDirectory();
return Policy.FromFile(Path.Combine(currentDir, @"resources\antisamy.xml"));
}
[Fact]
public void scriptAttacks()
{
List<string> list = new List<string>();
if (!list.Any(i => i == "s"))
{
}
var policy = GetTestPolicy();
_sut.Scan("test<script>alert(document.cookie)</script>", policy).CleanHtml.Contains("script").Should().BeFalse();
_sut.Scan("<<<><<script src=http://fake-evil.ru/test.js>", policy).CleanHtml.Contains("<script").Should().BeFalse();
_sut.Scan("<script<script src=http://fake-evil.ru/test.js>>", policy).CleanHtml.Contains("<script").Should().BeFalse();
_sut.Scan("<SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>", policy).CleanHtml.Contains("<script").Should().BeFalse();
_sut.Scan("<BODY onload!#$%&()*~+-_.,:;?@[/|\\]^`=alert(\"XSS\")>", policy).CleanHtml.Contains("onload").Should().BeFalse();
_sut.Scan("<BODY ONLOAD=alert('XSS')>", policy).CleanHtml.Contains("alert").Should().BeFalse();
_sut.Scan("<iframe src=http://ha.ckers.org/scriptlet.html <", policy).CleanHtml.Contains("<iframe").Should().BeFalse();
_sut.Scan("<INPUT TYPE=\"IMAGE\" SRC=\"javascript:alert('XSS');\">", policy).CleanHtml.Contains("src").Should().BeFalse();
_sut.Scan("<a onblur=\"alert(secret)\" href=\"http://www.google.com\">Google</a>", policy);
}
[Fact]
public void imgAttacks()
{
var policy = GetTestPolicy();
_sut.Scan("<img src=\"http://www.myspace.com/img.gif\"/>", policy).CleanHtml.Contains("<img").Should().BeTrue();
_sut.Scan("<img src=javascript:alert(document.cookie)>", policy).CleanHtml.Contains("<img").Should().BeFalse();
_sut.Scan("<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>", policy)
.CleanHtml.Contains("<img").Should().BeFalse();
_sut.Scan("<IMG SRC='&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041'>", policy)
.CleanHtml.Contains("<img").Should().BeFalse();
_sut.Scan("<IMG SRC=\"jav&#x0D;ascript:alert('XSS');\">", policy).CleanHtml.Contains("alert").Should().BeFalse();
string s = _sut.Scan("<IMG SRC=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>", policy).CleanHtml;
assertTrue(s.Length == 0 || s.Contains("&amp;"));
_sut.Scan("<IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>", policy);
_sut.Scan("<IMG SRC=\"javascript:alert('XSS')\"", policy).CleanHtml.Contains("javascript").Should().BeFalse();
_sut.Scan("<IMG LOWSRC=\"javascript:alert('XSS')\">", policy).CleanHtml.Contains("javascript").Should().BeFalse();
_sut.Scan("<BGSOUND SRC=\"javascript:alert('XSS');\">", policy).CleanHtml.Contains("javascript").Should().BeFalse();
}
[Fact]
public void hrefAttacks()
{
var policy = GetTestPolicy();
_sut.Scan("<LINK REL=\"stylesheet\" HREF=\"javascript:alert('XSS');\">", policy).CleanHtml.Contains("href").Should().BeFalse();
_sut.Scan("<LINK REL=\"stylesheet\" HREF=\"http://ha.ckers.org/xss.css\">", policy).CleanHtml.Contains("href").Should().BeFalse();
_sut.Scan("<STYLE>@import'http://ha.ckers.org/xss.css';</STYLE>", policy).CleanHtml.Contains("ha.ckers.org").Should().BeFalse();
_sut.Scan("<STYLE>BODY{-moz-binding:url(\"http://ha.ckers.org/xssmoz.xml#xss\")}</STYLE>", policy).CleanHtml.Contains("ha.ckers.org").Should().BeFalse();
_sut.Scan("<STYLE>li {list-style-image: url(\"javascript:alert('XSS')\");}</STYLE><UL><LI>XSS", policy).CleanHtml.Contains("javascript").Should().BeFalse();
_sut.Scan("<IMG SRC='vbscript:msgbox(\"XSS\")'>", policy).CleanHtml.Contains("vbscript").Should().BeFalse();
_sut.Scan("<META HTTP-EQUIV=\"refresh\" CONTENT=\"0; URL=http://;URL=javascript:alert('XSS');\">", policy).CleanHtml.Contains("<meta").Should().BeFalse();
_sut.Scan("<META HTTP-EQUIV=\"refresh\" CONTENT=\"0;url=javascript:alert('XSS');\">", policy).CleanHtml.Contains("<meta").Should().BeFalse();
_sut.Scan("<META HTTP-EQUIV=\"refresh\" CONTENT=\"0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K\">", policy).CleanHtml.Contains("<meta").Should().BeFalse();
_sut.Scan("<FRAMESET><FRAME SRC=\"javascript:alert('XSS');\"></FRAMESET>", policy).CleanHtml.Contains("javascript").Should().BeFalse();
_sut.Scan("<TABLE BACKGROUND=\"javascript:alert('XSS')\">", policy).CleanHtml.Contains("background").Should().BeFalse();
_sut.Scan("<TABLE><TD BACKGROUND=\"javascript:alert('XSS')\">", policy).CleanHtml.Contains("background").Should().BeFalse();
_sut.Scan("<DIV STYLE=\"background-image: url(javascript:alert('XSS'))\">", policy).CleanHtml.Contains("javascript").Should().BeFalse();
_sut.Scan("<DIV STYLE=\"width: expression(alert('XSS'));\">", policy).CleanHtml.Contains("alert").Should().BeFalse();
_sut.Scan("<IMG STYLE=\"xss:expr/*XSS*/ession(alert('XSS'))\">", policy).CleanHtml.Contains("alert").Should().BeFalse();
_sut.Scan("<STYLE>@im\\port'\\ja\\vasc\\ript:alert(\"XSS\")';</STYLE>", policy).CleanHtml.Contains("ript:alert").Should().BeFalse();
_sut.Scan("<BASE HREF=\"javascript:alert('XSS');//\">", policy).CleanHtml.Contains("javascript").Should().BeFalse();
_sut.Scan("<BaSe hReF=\"http://arbitrary.com/\">", policy).CleanHtml.Contains("<base").Should().BeFalse();
_sut.Scan("<OBJECT TYPE=\"text/x-scriptlet\" DATA=\"http://ha.ckers.org/scriptlet.html\"></OBJECT>", policy).CleanHtml.Contains("<object").Should().BeFalse();
_sut.Scan("<OBJECT classid=clsid:ae24fdae-03c6-11d1-8b76-0080c744f389><param name=url value=javascript:alert('XSS')></OBJECT>", policy).CleanHtml.Contains("jaascript").Should().BeFalse();
_sut.Scan("<EMBED SRC=\"http://ha.ckers.org/xss.swf\" AllowScriptAccess=\"always\"></EMBED>", policy).CleanHtml.Contains("<embed").Should().BeFalse();
_sut.Scan("<EMBED SRC=\"data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dH A6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv MjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hs aW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAw IiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlh TUyIpOzwvc2NyaXB0Pjwvc3ZnPg==\" type=\"image/svg+xml\" AllowScriptAccess=\"always\"></EMBED>", policy).CleanHtml.Contains("<embed").Should().BeFalse();
_sut.Scan("<SCRIPT a=\">\" SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>", policy).CleanHtml.Contains("<script").Should().BeFalse();
_sut.Scan("<SCRIPT a=\">\" '' SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>", policy).CleanHtml.Contains("<script").Should().BeFalse();
_sut.Scan("<SCRIPT a=`>` SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>", policy).CleanHtml.Contains("<script").Should().BeFalse();
_sut.Scan("<SCRIPT a=\">'>\" SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>", policy).CleanHtml.Contains("<script").Should().BeFalse();
_sut.Scan("<SCRIPT>document.write(\"<SCRI\");</SCRIPT>PT SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT>", policy).CleanHtml.Contains("script").Should().BeFalse();
_sut.Scan("<SCRIPT SRC=http://ha.ckers.org/xss.js", policy).CleanHtml.Contains("<script").Should().BeFalse();
_sut.Scan("<div/style=&#92&#45&#92&#109&#111&#92&#122&#92&#45&#98&#92&#105&#92&#110&#100&#92&#105&#110&#92&#103:&#92&#117&#114&#108&#40&#47&#47&#98&#117&#115&#105&#110&#101&#115&#115&#92&#105&#92&#110&#102&#111&#46&#99&#111&#46&#117&#107&#92&#47&#108&#97&#98&#115&#92&#47&#120&#98&#108&#92&#47&#120&#98&#108&#92&#46&#120&#109&#108&#92&#35&#120&#115&#115&#41&>", policy).CleanHtml.Contains("style").Should().BeFalse();
_sut.Scan("<a href='aim: &c:\\windows\\system32\\calc.exe' ini='C:\\Documents and Settings\\All Users\\Start Menu\\Programs\\Startup\\pwnd.bat'>", policy).CleanHtml.Contains("aim.exe").Should().BeFalse();
_sut.Scan("<!--\n<A href=\n- --><a href=javascript:alert:document.domain>test-->", policy).CleanHtml.Contains("javascript").Should().BeFalse();
_sut.Scan("<a></a style=\"\"xx:expr/**/ession(document.appendChild(document.createElement('script')).src='http://h4k.in/i.js')\">", policy).CleanHtml.Contains("document").Should().BeFalse();
_sut.Scan("<IFRAME SRC=\"javascript:alert('XSS');\"></IFRAME>", policy).CleanHtml.Contains("iframe").Should().BeFalse();
}
private void assertTrue(bool value)
{
value.Should().BeTrue();
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,203 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
W3C rules retrieved from:
http://www.w3.org/TR/html401/struct/global.html
-->
<!--
Slashdot allowed tags taken from "Reply" page:
<b> <i> <p> <br> <a> <ol> <ul> <li> <dl> <dt> <dd> <em> <strong> <tt> <blockquote> <div> <ecode> <quote>
-->
<anti-samy-rules xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="antisamy.xsd">
<directives>
<directive name="omitXmlDeclaration" value="true"/>
<directive name="omitDoctypeDeclaration" value="true"/>
<directive name="maxInputSize" value="5000"/>
<directive name="useXHTML" value="true"/>
<directive name="formatOutput" value="true"/>
<directive name="embedStyleSheets" value="false"/>
</directives>
<common-regexps>
<!--
From W3C:
This attribute assigns a class name or set of class names to an
element. Any number of elements may be assigned the same class
name or names. Multiple class names must be separated by white
space characters.
-->
<regexp name="htmlTitle" value="[\p{L}\p{N}\s\-_',:\[\]!\./\\\(\)&amp;]*"/> <!-- force non-empty with a '+' at the end instead of '*' -->
<regexp name="onsiteURL" value="^(?![\p{L}\p{N}\\\.\#@\$%\+&amp;;\-_~,\?=/!]*(&amp;colon))[\p{L}\p{N}\\\.\#@\$%\+&amp;;\-_~,\?=/!]*"/>
<regexp name="offsiteURL" value="(\s)*((ht|f)tp(s?)://|mailto:)[\p{L}\p{N}]+[~\p{L}\p{N}\p{Zs}\-_\.@\#\$%&amp;;:,\?=/\+!\(\)]*(\s)*"/>
</common-regexps>
<!--
Tag.name = a, b, div, body, etc.
Tag.action = filter: remove tags, but keep content, validate: keep content as long as it passes rules, remove: remove tag and contents
Attribute.name = id, class, href, align, width, etc.
Attribute.onInvalid = what to do when the attribute is invalid, e.g., remove the tag (removeTag), remove the attribute (removeAttribute), filter the tag (filterTag)
Attribute.description = What rules in English you want to tell the users they can have for this attribute. Include helpful things so they'll be able to tune their HTML
-->
<!--
Some attributes are common to all (or most) HTML tags. There aren't many that qualify for this. You have to make sure there's no
collisions between any of these attribute names with attribute names of other tags that are for different purposes.
-->
<common-attributes>
<attribute name="lang" description="The 'lang' attribute tells the browser what language the element's attribute values and content are written in">
<regexp-list>
<regexp value="[a-zA-Z]{2,20}"/>
</regexp-list>
</attribute>
<attribute name="title" description="The 'title' attribute provides text that shows up in a 'tooltip' when a user hovers their mouse over the element">
<regexp-list>
<regexp name="htmlTitle"/>
</regexp-list>
</attribute>
<attribute name="href" onInvalid="filterTag">
<regexp-list>
<regexp name="onsiteURL"/>
<regexp name="offsiteURL"/>
</regexp-list>
</attribute>
<attribute name="align" description="The 'align' attribute of an HTML element is a direction word, like 'left', 'right' or 'center'">
<literal-list>
<literal value="center"/>
<literal value="left"/>
<literal value="right"/>
<literal value="justify"/>
<literal value="char"/>
</literal-list>
</attribute>
</common-attributes>
<!--
This requires normal updates as browsers continue to diverge from the W3C and each other. As long as the browser wars continue
this is going to continue. I'm not sure war is the right word for what's going on. Doesn't somebody have to win a war after
a while?
-->
<global-tag-attributes>
<attribute name="title"/>
<attribute name="lang"/>
</global-tag-attributes>
<tags-to-encode>
<tag>g</tag>
<tag>grin</tag>
</tags-to-encode>
<tag-rules>
<!-- Tags related to JavaScript -->
<tag name="script" action="remove"/>
<tag name="noscript" action="remove"/>
<!-- Frame & related tags -->
<tag name="iframe" action="remove"/>
<tag name="frameset" action="remove"/>
<tag name="frame" action="remove"/>
<tag name="noframes" action="remove"/>
<!-- CSS related tags -->
<tag name="style" action="remove"/>
<!-- All reasonable formatting tags -->
<tag name="p" action="validate">
<attribute name="align"/>
</tag>
<tag name="div" action="validate"/>
<tag name="i" action="validate"/>
<tag name="b" action="validate"/>
<tag name="em" action="validate"/>
<tag name="blockquote" action="validate"/>
<tag name="tt" action="validate"/>
<tag name="strong" action="validate"/>
<tag name="br" action="truncate"/>
<!-- Custom Slashdot tags, though we're trimming the idea of having a possible mismatching end tag with the endtag="" attribute -->
<tag name="quote" action="validate"/>
<tag name="ecode" action="validate"/>
<!-- Anchor and anchor related tags -->
<tag name="a" action="validate">
<attribute name="href" onInvalid="filterTag"/>
<attribute name="nohref">
<literal-list>
<literal value="nohref"/>
<literal value=""/>
</literal-list>
</attribute>
<attribute name="rel">
<literal-list>
<literal value="nofollow"/>
</literal-list>
</attribute>
</tag>
<!-- List tags -->
<tag name="ul" action="validate"/>
<tag name="ol" action="validate"/>
<tag name="li" action="validate"/>
</tag-rules>
<!-- No CSS on Slashdot posts -->
<css-rules>
</css-rules>
<allowed-empty-tags>
<literal-list>
<literal value="br"/>
<literal value="hr"/>
<literal value="a"/>
<literal value="img"/>
<literal value="link"/>
<literal value="iframe"/>
<literal value="script"/>
<literal value="object"/>
<literal value="applet"/>
<literal value="frame"/>
<literal value="base"/>
<literal value="param"/>
<literal value="meta"/>
<literal value="input"/>
<literal value="textarea"/>
<literal value="embed"/>
<literal value="basefont"/>
<literal value="col"/>
<literal value="div"/>
</literal-list>
</allowed-empty-tags>
</anti-samy-rules>

View File

@@ -0,0 +1,225 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
TinyMCE
-->
<anti-samy-rules xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="antisamy.xsd">
<directives>
<directive name="omitXmlDeclaration" value="true" />
<directive name="omitDoctypeDeclaration" value="false" />
<directive name="maxInputSize" value="100000" />
<directive name="embedStyleSheets" value="false" />
<directive name="useXHTML" value="true" />
<directive name="formatOutput" value="true" />
</directives>
<common-regexps>
<!--
From W3C:
This attribute assigns a class name or set of class names to an
element. Any number of elements may be assigned the same class
name or names. Multiple class names must be separated by white
space characters.
-->
<regexp name="htmlTitle" value="[a-zA-Z0-9\s\-_',:\[\]!\./\\\(\)&amp;]*" />
<!-- force non-empty with a '+' at the end instead of '*'
-->
<regexp name="onsiteURL" value="^(?![\p{L}\p{N}\\\.\#@\$%\+&amp;;\-_~,\?=/!]*(&amp;colon))[\p{L}\p{N}\\\.\#@\$%\+&amp;;\-_~,\?=/!]*"/>
<!-- ([\w\\/\.\?=&amp;;\#-~]+|\#(\w)+)
-->
<!-- ([\p{L}/ 0-9&amp;\#-.?=])*
-->
<regexp name="offsiteURL" value="(\s)*((ht|f)tp(s?)://|mailto:)[A-Za-z0-9]+[~a-zA-Z0-9-_\.@\#\$%&amp;;:,\?=/\+!\(\)]*(\s)*" />
</common-regexps>
<!--
Tag.name = a, b, div, body, etc.
Tag.action = filter: remove tags, but keep content, validate: keep content as long as it passes rules, remove: remove tag and contents
Attribute.name = id, class, href, align, width, etc.
Attribute.onInvalid = what to do when the attribute is invalid, e.g., remove the tag (removeTag), remove the attribute (removeAttribute), filter the tag (filterTag)
Attribute.description = What rules in English you want to tell the users they can have for this attribute. Include helpful things so they'll be able to tune their HTML
-->
<!--
Some attributes are common to all (or most) HTML tags. There aren't many that qualify for this. You have to make sure there's no
collisions between any of these attribute names with attribute names of other tags that are for different purposes.
-->
<common-attributes>
<attribute name="lang"
description="The 'lang' attribute tells the browser what language the element's attribute values and content are written in">
<regexp-list>
<regexp value="[a-zA-Z]{2,20}" />
</regexp-list>
</attribute>
<attribute name="title"
description="The 'title' attribute provides text that shows up in a 'tooltip' when a user hovers their mouse over the element">
<regexp-list>
<regexp name="htmlTitle" />
</regexp-list>
</attribute>
<attribute name="href" onInvalid="filterTag">
<regexp-list>
<regexp name="onsiteURL" />
<regexp name="offsiteURL" />
<!--
-->
</regexp-list>
</attribute>
<attribute name="align"
description="The 'align' attribute of an HTML element is a direction word, like 'left', 'right' or 'center'">
<literal-list>
<literal value="center" />
<literal value="left" />
<literal value="right" />
<literal value="justify" />
<literal value="char" />
</literal-list>
</attribute>
<attribute name="style"
description="The 'style' attribute provides the ability for users to change many attributes of the tag's contents using a strict syntax" />
</common-attributes>
<!--
This requires normal updates as browsers continue to diverge from the W3C and each other. As long as the browser wars continue
this is going to continue. I'm not sure war is the right word for what's going on. Doesn't somebody have to win a war after
a while?
-->
<global-tag-attributes>
<attribute name="title" />
<attribute name="lang" />
<attribute name="style" />
</global-tag-attributes>
<tags-to-encode>
<tag>g</tag>
<tag>grin</tag>
</tags-to-encode>
<tag-rules>
<!-- Remove -->
<tag name="script" action="remove" />
<tag name="noscript" action="remove" />
<tag name="iframe" action="remove" />
<tag name="frameset" action="remove" />
<tag name="frame" action="remove" />
<tag name="noframes" action="remove" />
<tag name="head" action="remove" />
<tag name="title" action="remove" />
<tag name="base" action="remove" />
<tag name="style" action="remove" />
<tag name="link" action="remove" />
<tag name="input" action="remove" />
<tag name="textarea" action="remove" />
<!-- Truncate -->
<tag name="br" action="truncate" />
<!-- Validate -->
<tag name="p" action="validate">
<attribute name="align" />
</tag>
<tag name="div" action="validate" />
<tag name="span" action="validate" />
<tag name="i" action="validate" />
<tag name="b" action="validate" />
<tag name="strong" action="validate" />
<tag name="s" action="validate" />
<tag name="strike" action="validate" />
<tag name="u" action="validate" />
<tag name="em" action="validate" />
<tag name="blockquote" action="validate" />
<tag name="tt" action="truncate" />
<tag name="a" action="validate">
<attribute name="href" onInvalid="filterTag" />
<attribute name="nohref">
<literal-list>
<literal value="nohref" />
<literal value="" />
</literal-list>
</attribute>
<attribute name="rel">
<literal-list>
<literal value="nofollow" />
</literal-list>
</attribute>
</tag>
<!-- List tags
-->
<tag name="ul" action="validate" />
<tag name="ol" action="validate" />
<tag name="li" action="validate" />
<tag name="dl" action="validate" />
<tag name="dt" action="validate" />
<tag name="dd" action="validate" />
</tag-rules>
<css-rules>
<property name="text-decoration" default="none"
description="">
<category-list>
<category value="visual" />
</category-list>
<literal-list>
<literal value="underline" />
<literal value="overline" />
<literal value="line-through" />
</literal-list>
</property>
</css-rules>
<allowed-empty-tags>
<literal-list>
<literal value="br"/>
<literal value="hr"/>
<literal value="a"/>
<literal value="img"/>
<literal value="link"/>
<literal value="iframe"/>
<literal value="script"/>
<literal value="object"/>
<literal value="applet"/>
<literal value="frame"/>
<literal value="base"/>
<literal value="param"/>
<literal value="meta"/>
<literal value="input"/>
<literal value="textarea"/>
<literal value="embed"/>
<literal value="basefont"/>
<literal value="col"/>
<literal value="div"/>
</literal-list>
</allowed-empty-tags>
</anti-samy-rules>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="anti-samy-rules">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="directives" type="Directives" maxOccurs="1" minOccurs="1"/>
<xsd:element name="common-regexps" type="CommonRegexps" maxOccurs="1" minOccurs="1"/>
<xsd:element name="common-attributes" type="AttributeList" maxOccurs="1" minOccurs="1"/>
<xsd:element name="global-tag-attributes" type="AttributeList" maxOccurs="1" minOccurs="1"/>
<xsd:element name="tags-to-encode" type="TagsToEncodeList" minOccurs="0" maxOccurs="1"/>
<xsd:element name="tag-rules" type="TagRules" minOccurs="1" maxOccurs="1"/>
<xsd:element name="css-rules" type="CSSRules" minOccurs="1" maxOccurs="1"/>
<xsd:element name="allowed-empty-tags" type="AllowedEmptyTags" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="Directives">
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="directive" type="Directive" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="Directive">
<xsd:attribute name="name" use="required"/>
<xsd:attribute name="value" use="required"/>
</xsd:complexType>
<xsd:complexType name="CommonRegexps">
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="regexp" type="RegExp" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="AttributeList">
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="attribute" type="Attribute" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="TagsToEncodeList">
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="tag" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="TagRules">
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="tag" type="Tag" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="Tag">
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="attribute" type="Attribute" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required"/>
<xsd:attribute name="action" use="required"/>
</xsd:complexType>
<xsd:complexType name="AllowedEmptyTags">
<xsd:sequence>
<xsd:element name="literal-list" type="LiteralList" minOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="Attribute">
<xsd:sequence>
<xsd:element name="regexp-list" type="RegexpList" minOccurs="0"/>
<xsd:element name="literal-list" type="LiteralList" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required"/>
<xsd:attribute name="description"/>
<xsd:attribute name="onInvalid"/>
</xsd:complexType>
<xsd:complexType name="RegexpList">
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="regexp" type="RegExp" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="RegExp">
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="value" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="LiteralList">
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="literal" type="Literal" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="Literal">
<xsd:attribute name="value" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="CSSRules">
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="property" type="Property" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="Property">
<xsd:sequence>
<xsd:element name="category-list" type="CategoryList" minOccurs="0"/>
<xsd:element name="literal-list" type="LiteralList" minOccurs="0"/>
<xsd:element name="regexp-list" type="RegexpList" minOccurs="0"/>
<xsd:element name="shorthand-list" type="ShorthandList" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="default" type="xsd:string"/>
<xsd:attribute name="description" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="ShorthandList">
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="shorthand" type="Shorthand" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="Shorthand">
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
<xsd:complexType name="CategoryList">
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="category" type="Category" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="Category">
<xsd:attribute name="value" type="xsd:string" use="required"/>
</xsd:complexType>
<xsd:complexType name="Entity">
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="cdata" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:schema>