Initial commit
This commit is contained in:
35
AntiSamy.sln
35
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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,8 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace AntiSamy
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
}
|
||||
}
|
||||
21
LICENCE
Normal file
21
LICENCE
Normal 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
30
README.md
Normal 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
10
appveyor.yml
Normal 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
133
build.cake
Normal 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
134
build.ps1
Normal 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
28
common.props
Normal 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>
|
||||
26
src/AntiSamy/AntiSamy.cs
Normal file
26
src/AntiSamy/AntiSamy.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
12
src/AntiSamy/AntiSamy.csproj
Normal file
12
src/AntiSamy/AntiSamy.csproj
Normal 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>
|
||||
373
src/AntiSamy/AntiSamyDomScanner.cs
Normal file
373
src/AntiSamy/AntiSamyDomScanner.cs
Normal 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 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 <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();
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/AntiSamy/AntiySamyResult.cs
Normal file
21
src/AntiSamy/AntiySamyResult.cs
Normal 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
24
src/AntiSamy/Consts.cs
Normal 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
205
src/AntiSamy/CssScanner.cs
Normal 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*([^\/#]*?)(?:\:|�*58|�*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;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/AntiSamy/HtmlEntityEncoder.cs
Normal file
49
src/AntiSamy/HtmlEntityEncoder.cs
Normal 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("&");
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/AntiSamy/Model/CssProperty.cs
Normal file
29
src/AntiSamy/Model/CssProperty.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
109
src/AntiSamy/Model/DocumentAttribute.cs
Normal file
109
src/AntiSamy/Model/DocumentAttribute.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
74
src/AntiSamy/Model/DocumentTag.cs
Normal file
74
src/AntiSamy/Model/DocumentTag.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
17
src/AntiSamy/ParseException.cs
Normal file
17
src/AntiSamy/ParseException.cs
Normal 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
431
src/AntiSamy/Policy.cs
Normal 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
|
||||
}
|
||||
}
|
||||
17
src/AntiSamy/PolicyException.cs
Normal file
17
src/AntiSamy/PolicyException.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/AntiSamy/ScanException.cs
Normal file
17
src/AntiSamy/ScanException.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
49
test/AntiSamy.Tests/AntiSamy.Tests.csproj
Normal file
49
test/AntiSamy.Tests/AntiSamy.Tests.csproj
Normal 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>
|
||||
183
test/AntiSamy.Tests/AntiSamyTests.cs
Normal file
183
test/AntiSamy.Tests/AntiSamyTests.cs
Normal 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=javascript:alert('XSS')>", policy)
|
||||
.CleanHtml.Contains("<img").Should().BeFalse();
|
||||
|
||||
|
||||
_sut.Scan("<IMG SRC='javascript:alert('XSS')'>", policy)
|
||||
.CleanHtml.Contains("<img").Should().BeFalse();
|
||||
|
||||
|
||||
_sut.Scan("<IMG SRC=\"jav
ascript:alert('XSS');\">", policy).CleanHtml.Contains("alert").Should().BeFalse();
|
||||
|
||||
string s = _sut.Scan("<IMG SRC=javascript:alert('XSS')>", policy).CleanHtml;
|
||||
assertTrue(s.Length == 0 || s.Contains("&"));
|
||||
|
||||
|
||||
_sut.Scan("<IMG SRC=javascript:alert('XSS')>", 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=\-\mo\z\-b\i\nd\in\g:\url(//business\i\nfo.co.uk\/labs\/xbl\/xbl\.xml\#xss)&>", 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2659
test/AntiSamy.Tests/resources/antisamy-anythinggoes.xml
Normal file
2659
test/AntiSamy.Tests/resources/antisamy-anythinggoes.xml
Normal file
File diff suppressed because it is too large
Load Diff
2467
test/AntiSamy.Tests/resources/antisamy-ebay.xml
Normal file
2467
test/AntiSamy.Tests/resources/antisamy-ebay.xml
Normal file
File diff suppressed because it is too large
Load Diff
2624
test/AntiSamy.Tests/resources/antisamy-myspace.xml
Normal file
2624
test/AntiSamy.Tests/resources/antisamy-myspace.xml
Normal file
File diff suppressed because it is too large
Load Diff
203
test/AntiSamy.Tests/resources/antisamy-slashdot.xml
Normal file
203
test/AntiSamy.Tests/resources/antisamy-slashdot.xml
Normal 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\-_',:\[\]!\./\\\(\)&]*"/> <!-- force non-empty with a '+' at the end instead of '*' -->
|
||||
<regexp name="onsiteURL" value="^(?![\p{L}\p{N}\\\.\#@\$%\+&;\-_~,\?=/!]*(&colon))[\p{L}\p{N}\\\.\#@\$%\+&;\-_~,\?=/!]*"/>
|
||||
<regexp name="offsiteURL" value="(\s)*((ht|f)tp(s?)://|mailto:)[\p{L}\p{N}]+[~\p{L}\p{N}\p{Zs}\-_\.@\#\$%&;:,\?=/\+!\(\)]*(\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>
|
||||
225
test/AntiSamy.Tests/resources/antisamy-tinymce.xml
Normal file
225
test/AntiSamy.Tests/resources/antisamy-tinymce.xml
Normal 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\-_',:\[\]!\./\\\(\)&]*" />
|
||||
|
||||
<!-- force non-empty with a '+' at the end instead of '*'
|
||||
-->
|
||||
<regexp name="onsiteURL" value="^(?![\p{L}\p{N}\\\.\#@\$%\+&;\-_~,\?=/!]*(&colon))[\p{L}\p{N}\\\.\#@\$%\+&;\-_~,\?=/!]*"/>
|
||||
|
||||
<!-- ([\w\\/\.\?=&;\#-~]+|\#(\w)+)
|
||||
-->
|
||||
|
||||
<!-- ([\p{L}/ 0-9&\#-.?=])*
|
||||
-->
|
||||
<regexp name="offsiteURL" value="(\s)*((ht|f)tp(s?)://|mailto:)[A-Za-z0-9]+[~a-zA-Z0-9-_\.@\#\$%&;:,\?=/\+!\(\)]*(\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>
|
||||
2756
test/AntiSamy.Tests/resources/antisamy.xml
Normal file
2756
test/AntiSamy.Tests/resources/antisamy.xml
Normal file
File diff suppressed because it is too large
Load Diff
152
test/AntiSamy.Tests/resources/antisamy.xsd
Normal file
152
test/AntiSamy.Tests/resources/antisamy.xsd
Normal 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>
|
||||
Reference in New Issue
Block a user