From de57355d06f4263a1ae489c0a5fcca3b73106a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caner=20Pat=C4=B1r?= Date: Fri, 4 May 2018 15:39:39 +0300 Subject: [PATCH] Initial commit --- AntiSamy.sln | 35 +- AntiSamy/AntiSamy.csproj | 7 - AntiSamy/Class1.cs | 8 - LICENCE | 21 + README.md | 30 + appveyor.yml | 10 + build.cake | 133 + build.ps1 | 134 + common.props | 28 + icon.png | Bin 0 -> 51542 bytes src/AntiSamy/AntiSamy.cs | 26 + src/AntiSamy/AntiSamy.csproj | 12 + src/AntiSamy/AntiSamyDomScanner.cs | 373 +++ src/AntiSamy/AntiySamyResult.cs | 21 + src/AntiSamy/Consts.cs | 24 + src/AntiSamy/CssScanner.cs | 205 ++ src/AntiSamy/HtmlEntityEncoder.cs | 49 + src/AntiSamy/Model/CssProperty.cs | 29 + src/AntiSamy/Model/DocumentAttribute.cs | 109 + src/AntiSamy/Model/DocumentTag.cs | 74 + src/AntiSamy/ParseException.cs | 17 + src/AntiSamy/Policy.cs | 431 +++ src/AntiSamy/PolicyException.cs | 17 + src/AntiSamy/ScanException.cs | 17 + test/AntiSamy.Tests/AntiSamy.Tests.csproj | 49 + test/AntiSamy.Tests/AntiSamyTests.cs | 183 ++ .../resources/antisamy-anythinggoes.xml | 2659 ++++++++++++++++ .../resources/antisamy-ebay.xml | 2467 +++++++++++++++ .../resources/antisamy-myspace.xml | 2624 ++++++++++++++++ .../resources/antisamy-slashdot.xml | 203 ++ .../resources/antisamy-tinymce.xml | 225 ++ test/AntiSamy.Tests/resources/antisamy.xml | 2756 +++++++++++++++++ test/AntiSamy.Tests/resources/antisamy.xsd | 152 + 33 files changed, 13108 insertions(+), 20 deletions(-) delete mode 100644 AntiSamy/AntiSamy.csproj delete mode 100644 AntiSamy/Class1.cs create mode 100644 LICENCE create mode 100644 README.md create mode 100644 appveyor.yml create mode 100644 build.cake create mode 100644 build.ps1 create mode 100644 common.props create mode 100644 icon.png create mode 100644 src/AntiSamy/AntiSamy.cs create mode 100644 src/AntiSamy/AntiSamy.csproj create mode 100644 src/AntiSamy/AntiSamyDomScanner.cs create mode 100644 src/AntiSamy/AntiySamyResult.cs create mode 100644 src/AntiSamy/Consts.cs create mode 100644 src/AntiSamy/CssScanner.cs create mode 100644 src/AntiSamy/HtmlEntityEncoder.cs create mode 100644 src/AntiSamy/Model/CssProperty.cs create mode 100644 src/AntiSamy/Model/DocumentAttribute.cs create mode 100644 src/AntiSamy/Model/DocumentTag.cs create mode 100644 src/AntiSamy/ParseException.cs create mode 100644 src/AntiSamy/Policy.cs create mode 100644 src/AntiSamy/PolicyException.cs create mode 100644 src/AntiSamy/ScanException.cs create mode 100644 test/AntiSamy.Tests/AntiSamy.Tests.csproj create mode 100644 test/AntiSamy.Tests/AntiSamyTests.cs create mode 100644 test/AntiSamy.Tests/resources/antisamy-anythinggoes.xml create mode 100644 test/AntiSamy.Tests/resources/antisamy-ebay.xml create mode 100644 test/AntiSamy.Tests/resources/antisamy-myspace.xml create mode 100644 test/AntiSamy.Tests/resources/antisamy-slashdot.xml create mode 100644 test/AntiSamy.Tests/resources/antisamy-tinymce.xml create mode 100644 test/AntiSamy.Tests/resources/antisamy.xml create mode 100644 test/AntiSamy.Tests/resources/antisamy.xsd diff --git a/AntiSamy.sln b/AntiSamy.sln index ee12083..04dd872 100644 --- a/AntiSamy.sln +++ b/AntiSamy.sln @@ -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 diff --git a/AntiSamy/AntiSamy.csproj b/AntiSamy/AntiSamy.csproj deleted file mode 100644 index 9f5c4f4..0000000 --- a/AntiSamy/AntiSamy.csproj +++ /dev/null @@ -1,7 +0,0 @@ - - - - netstandard2.0 - - - diff --git a/AntiSamy/Class1.cs b/AntiSamy/Class1.cs deleted file mode 100644 index 881b90c..0000000 --- a/AntiSamy/Class1.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace AntiSamy -{ - public class Class1 - { - } -} diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..23780f2 --- /dev/null +++ b/LICENCE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..71641f8 --- /dev/null +++ b/README.md @@ -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("") +AntiSamy antiSamy = new AntiSamy(); +string yourDirtyInput = "
"; +AntiSamyResult result = antiSamy.Scan(yourDirtyInput, antiSamyPolicy); + +string cleanHtml = result.CleanHtml; +IEnumerable 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) \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..54ef9c5 --- /dev/null +++ b/appveyor.yml @@ -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 \ No newline at end of file diff --git a/build.cake b/build.cake new file mode 100644 index 0000000..df3b47c --- /dev/null +++ b/build.cake @@ -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> + { + new Tuple("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 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); \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..51ecee6 --- /dev/null +++ b/build.ps1 @@ -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 \ No newline at end of file diff --git a/common.props b/common.props new file mode 100644 index 0000000..8fda700 --- /dev/null +++ b/common.props @@ -0,0 +1,28 @@ + + + 1.0.1 + $(NoWarn);CS1591 + https://raw.githubusercontent.com/canerpatir/AntiSamy.NET/master/icon.png + https://github.com/canerpatir/AntiSamy.NET + https://github.com/canerpatir/AntiSamy.NET/blob/master/LICENCE + git + https://github.com/canerpatir/AntiSamy.NET + Oguzhan Soykan + True + false + false + false + false + false + false + false + false + true + true + 7.1 + + + + + + \ No newline at end of file diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3738813ad3548814db7bc0430b0b968b24f36b29 GIT binary patch literal 51542 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 OQ23 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 bhX4N?52lOk-^Tcc0SG)@{an^LB{Ts5cYI+6 literal 0 HcmV?d00001 diff --git a/src/AntiSamy/AntiSamy.cs b/src/AntiSamy/AntiSamy.cs new file mode 100644 index 0000000..bea5fe4 --- /dev/null +++ b/src/AntiSamy/AntiSamy.cs @@ -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); + } + + } +} diff --git a/src/AntiSamy/AntiSamy.csproj b/src/AntiSamy/AntiSamy.csproj new file mode 100644 index 0000000..52d6a7e --- /dev/null +++ b/src/AntiSamy/AntiSamy.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/src/AntiSamy/AntiSamyDomScanner.cs b/src/AntiSamy/AntiSamyDomScanner.cs new file mode 100644 index 0000000..22d1339 --- /dev/null +++ b/src/AntiSamy/AntiSamyDomScanner.cs @@ -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 _errorMessages = new List(); + + 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   getting double encoded, so this converts it to a literal space. + //this may need to be changed. + html = html.Replace(" ", 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 " + HtmlEntityEncoder.HtmlEntityEncode(tagName.ToLower()) + " "); + } + + 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 " + HtmlEntityEncoder.HtmlEntityEncode(tagName) + " tag contained an attribute that we couldn't process. "); + errBuff.Append("The " + HtmlEntityEncoder.HtmlEntityEncode(name) + " attribute had a value of " + HtmlEntityEncoder.HtmlEntityEncode(value) + ". "); + 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 " + HtmlEntityEncoder.HtmlEntityEncode(tagName) + " 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 " + HtmlEntityEncoder.HtmlEntityEncode(tagName) + " 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 " + HtmlEntityEncoder.HtmlEntityEncode(name) + " 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 " + HtmlEntityEncoder.HtmlEntityEncode(name)); + errBuff.Append(" attribute of the " + HtmlEntityEncoder.HtmlEntityEncode(tagName) + " 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 " + HtmlEntityEncoder.HtmlEntityEncode(nnmap[0].Name)); + errBuff.Append(" attribute of the " + HtmlEntityEncoder.HtmlEntityEncode(tagName) + " 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 " + HtmlEntityEncoder.HtmlEntityEncode(tagName) + " 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(); + } + } +} diff --git a/src/AntiSamy/AntiySamyResult.cs b/src/AntiSamy/AntiySamyResult.cs new file mode 100644 index 0000000..84d9102 --- /dev/null +++ b/src/AntiSamy/AntiySamyResult.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace AntiSamy +{ + public class AntiySamyResult + { + public AntiySamyResult(DateTime startOfScan, string cleanHtml, IEnumerable errorMessages) + { + Elapsed = DateTime.UtcNow - startOfScan; + CleanHtml = cleanHtml; + ErrorMessages = errorMessages; + } + + public TimeSpan Elapsed { get; } + + public string CleanHtml { get; } + + public IEnumerable ErrorMessages { get; } + } +} diff --git a/src/AntiSamy/Consts.cs b/src/AntiSamy/Consts.cs new file mode 100644 index 0000000..5e65f0d --- /dev/null +++ b/src/AntiSamy/Consts.cs @@ -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 + } + } +} diff --git a/src/AntiSamy/CssScanner.cs b/src/AntiSamy/CssScanner.cs new file mode 100644 index 0000000..b596f00 --- /dev/null +++ b/src/AntiSamy/CssScanner.cs @@ -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 _errors = new List(); + + 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 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().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>(); + + 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(cssProperty, $"Css property \"{key}\" is not allowed")); + continue; + } + + if (dangerousCssExpressionTest.IsMatch(value)) + { + removingProperties.Add(new Tuple(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*([^\/#]*?)(?:\:|�*58|�*3a)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + if (!urls.Cast().All(u => schemeRegex.IsMatch(u.Value))) + { + removingProperties.Add(new Tuple(cssProperty, "Illegal url detected.")); + } + } + } + + foreach (Tuple style in removingProperties) + { + styles.RemoveProperty(style.Item1.Name); + _errors.Add(style.Item2); + } + } + + private void ValidateValue(CssProperty allowedCssProperty, ICssProperty cssProperty, string value, List> removeStyles) + { + if (!allowedCssProperty.AllowedLiterals.Any(lit => lit.Equals(value, StringComparison.OrdinalIgnoreCase))) + { + removeStyles.Add(new Tuple(cssProperty, $"\"{value}\" is not allowed literal")); + return; + } + + if (!allowedCssProperty.AllowedRegExps.Any(regex => new Regex(regex).IsMatch(value))) + { + removeStyles.Add(new Tuple(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; + } + } +} diff --git a/src/AntiSamy/HtmlEntityEncoder.cs b/src/AntiSamy/HtmlEntityEncoder.cs new file mode 100644 index 0000000..dc4f0f6 --- /dev/null +++ b/src/AntiSamy/HtmlEntityEncoder.cs @@ -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("&"); + break; + case '<': + sb.Append("<"); + break; + case '>': + sb.Append(">"); + 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(); + } + } +} diff --git a/src/AntiSamy/Model/CssProperty.cs b/src/AntiSamy/Model/CssProperty.cs new file mode 100644 index 0000000..e1b6eb5 --- /dev/null +++ b/src/AntiSamy/Model/CssProperty.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace AntiSamy.Model +{ + public class CssProperty + { + public CssProperty(string name, IEnumerable allowedRegexps, IEnumerable allowedLiterals, IEnumerable shortHandRefs, string description, string onInvalid) + { + Description = description; + Name = name; + OnInvalid = onInvalid; + AllowedRegExps = allowedRegexps ?? new List(); + AllowedLiterals = allowedLiterals ?? new List(); + ShorthandRefs = shortHandRefs ?? new List(); + } + + public string Description { get; } + + public string Name { get; } + + public string OnInvalid { get; } + + public IEnumerable AllowedLiterals { get; } + + public IEnumerable AllowedRegExps { get; } + + public IEnumerable ShorthandRefs { get; } + } +} diff --git a/src/AntiSamy/Model/DocumentAttribute.cs b/src/AntiSamy/Model/DocumentAttribute.cs new file mode 100644 index 0000000..4891569 --- /dev/null +++ b/src/AntiSamy/Model/DocumentAttribute.cs @@ -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 AllowedValues { get; } = new List(); + + public List AllowedRegExps { get; } = new List(); + + + public DocumentAttribute(string name, List allowedRegexps, List 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) + { + //

+ + 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); + } +} diff --git a/src/AntiSamy/Model/DocumentTag.cs b/src/AntiSamy/Model/DocumentTag.cs new file mode 100644 index 0000000..039512d --- /dev/null +++ b/src/AntiSamy/Model/DocumentTag.cs @@ -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 _allowedAttributes = new Dictionary(); + + public DocumentTag(string name, string action) + { + Name = name; + Action = action; + } + + public string Name { get; } + + public IReadOnlyDictionary 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 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; + } +} diff --git a/src/AntiSamy/ParseException.cs b/src/AntiSamy/ParseException.cs new file mode 100644 index 0000000..060199e --- /dev/null +++ b/src/AntiSamy/ParseException.cs @@ -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) + { + } + } +} diff --git a/src/AntiSamy/Policy.cs b/src/AntiSamy/Policy.cs new file mode 100644 index 0000000..e177c8e --- /dev/null +++ b/src/AntiSamy/Policy.cs @@ -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 CommonAttributes { get; private set; } = new Dictionary(); + + public IReadOnlyDictionary CommonRegularExpressions { get; private set; } = new Dictionary(); + + public IReadOnlyDictionary CssRules { get; private set; } = new Dictionary(); + + public IReadOnlyDictionary TagRules { get; private set; } = new Dictionary(); + + public IReadOnlyDictionary Directives { get; private set; } = new Dictionary(); + + public IReadOnlyDictionary GlobalTagAttributes { get; private set; } = new Dictionary(); + + 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 ParseDirectives(XmlNode directiveListNode) + { + XmlNodeList directiveNodes = directiveListNode.SelectNodes("directive"); + var directives = new Dictionary(); + 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 ParseGlobalAttributes(XmlNode globalAttributeListNode) + { + XmlNodeList globalAttributeNodes = globalAttributeListNode.SelectNodes("attribute"); + var globalAttributes = new Dictionary(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 "); + } + + //if (!globalAttributes.ContainsKey(_name)) + // globalAttributes.Add(_name, new AntiSamyPattern(_name, _value)); + } + } + return globalAttributes; + } + + private Dictionary ParseCommonRegExps(XmlNode commonRegularExpressionListNode) + { + XmlNodeList list = commonRegularExpressionListNode.SelectNodes("regexp"); + var commonRegularExpressions = new Dictionary(); + 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 ParseCommonAttributes(XmlNode commonAttributeListNode) + { + XmlNodeList commonAttributeNodes = commonAttributeListNode.SelectNodes("attribute"); + var commonAttributes = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + foreach (XmlNode node in commonAttributeNodes) + { + if (node.NodeType == XmlNodeType.Element) + { + var allowedRegExp = new List(); + 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(); + 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 ParseTagRules(XmlNode tagAttributeListNode) + { + var tags = new Dictionary(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(); + 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 "); + } + } + else if (!string.IsNullOrEmpty(value)) + { + allowedRegExps.Add(RegexpBegin + value + RegexpEnd); + } + } + } + + var allowedValues = new List(); + 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 ParseCssRules(XmlNode cssNodeList) + { + var properties = new Dictionary(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(); + 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 "); + } + } + } + + var allowedLiterals = new List(); + 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(); + 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 + } +} diff --git a/src/AntiSamy/PolicyException.cs b/src/AntiSamy/PolicyException.cs new file mode 100644 index 0000000..77c5388 --- /dev/null +++ b/src/AntiSamy/PolicyException.cs @@ -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) + { + } + } +} diff --git a/src/AntiSamy/ScanException.cs b/src/AntiSamy/ScanException.cs new file mode 100644 index 0000000..48ed46a --- /dev/null +++ b/src/AntiSamy/ScanException.cs @@ -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) + { + } + } +} diff --git a/test/AntiSamy.Tests/AntiSamy.Tests.csproj b/test/AntiSamy.Tests/AntiSamy.Tests.csproj new file mode 100644 index 0000000..6dfe533 --- /dev/null +++ b/test/AntiSamy.Tests/AntiSamy.Tests.csproj @@ -0,0 +1,49 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/test/AntiSamy.Tests/AntiSamyTests.cs b/test/AntiSamy.Tests/AntiSamyTests.cs new file mode 100644 index 0000000..70eedfe --- /dev/null +++ b/test/AntiSamy.Tests/AntiSamyTests.cs @@ -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 + // "click here" + "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 list = new List(); + + if (!list.Any(i => i == "s")) + { + + } + + var policy = GetTestPolicy(); + + _sut.Scan("test", policy).CleanHtml.Contains("script").Should().BeFalse(); + + _sut.Scan("<<<><", policy).CleanHtml.Contains("", policy).CleanHtml.Contains("onload").Should().BeFalse(); + + _sut.Scan("", policy).CleanHtml.Contains("alert").Should().BeFalse(); + + _sut.Scan("", policy).CleanHtml.Contains("iframe").Should().BeFalse(); + } + + private void assertTrue(bool value) + { + value.Should().BeTrue(); + } + } +} diff --git a/test/AntiSamy.Tests/resources/antisamy-anythinggoes.xml b/test/AntiSamy.Tests/resources/antisamy-anythinggoes.xml new file mode 100644 index 0000000..8a98c28 --- /dev/null +++ b/test/AntiSamy.Tests/resources/antisamy-anythinggoes.xml @@ -0,0 +1,2659 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + g + grin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/AntiSamy.Tests/resources/antisamy-ebay.xml b/test/AntiSamy.Tests/resources/antisamy-ebay.xml new file mode 100644 index 0000000..9f4ef15 --- /dev/null +++ b/test/AntiSamy.Tests/resources/antisamy-ebay.xml @@ -0,0 +1,2467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + g + grin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/AntiSamy.Tests/resources/antisamy-myspace.xml b/test/AntiSamy.Tests/resources/antisamy-myspace.xml new file mode 100644 index 0000000..e3adbab --- /dev/null +++ b/test/AntiSamy.Tests/resources/antisamy-myspace.xml @@ -0,0 +1,2624 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + g + grin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/AntiSamy.Tests/resources/antisamy-slashdot.xml b/test/AntiSamy.Tests/resources/antisamy-slashdot.xml new file mode 100644 index 0000000..4e32d02 --- /dev/null +++ b/test/AntiSamy.Tests/resources/antisamy-slashdot.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + g + grin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/AntiSamy.Tests/resources/antisamy-tinymce.xml b/test/AntiSamy.Tests/resources/antisamy-tinymce.xml new file mode 100644 index 0000000..d565628 --- /dev/null +++ b/test/AntiSamy.Tests/resources/antisamy-tinymce.xml @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + g + grin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/AntiSamy.Tests/resources/antisamy.xml b/test/AntiSamy.Tests/resources/antisamy.xml new file mode 100644 index 0000000..9199cfc --- /dev/null +++ b/test/AntiSamy.Tests/resources/antisamy.xml @@ -0,0 +1,2756 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + g + grin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/AntiSamy.Tests/resources/antisamy.xsd b/test/AntiSamy.Tests/resources/antisamy.xsd new file mode 100644 index 0000000..95a3bb2 --- /dev/null +++ b/test/AntiSamy.Tests/resources/antisamy.xsd @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +