mirror of
https://github.com/MvDevsUnion/WPetition.git
synced 2026-01-25 23:09:28 +00:00
added SVG validation
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
using Ashi.MongoInterface.Service;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -54,7 +52,6 @@ namespace Submission.Api.Controllers
|
||||
|
||||
if (validation.Success)
|
||||
{
|
||||
//why??
|
||||
var cacheKey = $"petition_{petition_id}";
|
||||
|
||||
var pet = await _detailRepository.FindByIdAsync(petition_id);
|
||||
@@ -62,9 +59,11 @@ namespace Submission.Api.Controllers
|
||||
if (pet == null)
|
||||
return NotFound();
|
||||
|
||||
//TODO : add svg validation
|
||||
//fuck i still havent done this
|
||||
|
||||
// SVG validation: reject bad/malicious SVGs before persisting
|
||||
if (!Submission.Api.Services.SvgValidator.TryValidate(body.Signature, out var svgError))
|
||||
{
|
||||
return BadRequest($"Invalid signature SVG: {svgError}");
|
||||
}
|
||||
|
||||
//check to see if the same person signed the petition already
|
||||
//if dupe send error saying user already signed
|
||||
|
||||
169
Submission.Api/Services/SvgValidator.cs
Normal file
169
Submission.Api/Services/SvgValidator.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using System.Xml;
|
||||
|
||||
namespace Submission.Api.Services
|
||||
{
|
||||
public static class SvgValidator
|
||||
{
|
||||
// Tunable limits
|
||||
private const int MaxLength = 100_000; // max characters in SVG string
|
||||
private const int MaxElements = 5_000; // max number of XML elements
|
||||
|
||||
// Whitelist of allowed element local names (lowercase)
|
||||
private static readonly HashSet<string> AllowedElements = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"svg","g","path","rect","circle","ellipse","line","polyline","polygon",
|
||||
"text","tspan","defs","use","title","desc","clipPath","mask",
|
||||
"linearGradient","radialGradient","stop","style","metadata"
|
||||
};
|
||||
|
||||
// Basic attribute whitelist (prefix-free) - attributes not listed are still allowed but checked for danger.
|
||||
private static readonly HashSet<string> AllowedAttributes = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"id","class","width","height","viewBox","fill","stroke","d","x","y","cx","cy","r","rx","ry","points",
|
||||
"transform","style","xmlns","xmlns:xlink","xlink:href","href","opacity","stroke-width","font-size","font-family"
|
||||
};
|
||||
|
||||
public static bool TryValidate(string svgContent, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(svgContent))
|
||||
{
|
||||
error = "SVG content is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (svgContent.Length > MaxLength)
|
||||
{
|
||||
error = $"SVG is too large (>{MaxLength} characters)";
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimmed = svgContent.TrimStart();
|
||||
if (!(trimmed.StartsWith("<svg", StringComparison.OrdinalIgnoreCase) || trimmed.StartsWith("<?xml", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
error = "SVG must begin with <svg> or XML prolog";
|
||||
return false;
|
||||
}
|
||||
|
||||
var settings = new XmlReaderSettings
|
||||
{
|
||||
DtdProcessing = DtdProcessing.Prohibit,
|
||||
XmlResolver = null,
|
||||
MaxCharactersFromEntities = 1024
|
||||
};
|
||||
|
||||
int elementCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
using var sr = new StringReader(svgContent);
|
||||
using var reader = XmlReader.Create(sr, settings);
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
elementCount++;
|
||||
if (elementCount > MaxElements)
|
||||
{
|
||||
error = "SVG contains too many elements";
|
||||
return false;
|
||||
}
|
||||
|
||||
var localName = reader.LocalName ?? string.Empty;
|
||||
|
||||
// Disallow dangerous elements
|
||||
if (string.Equals(localName, "script", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(localName, "foreignObject", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(localName, "iframe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = $"Disallowed element: {localName}";
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
//if (!AllowedElements.Contains(localName))
|
||||
//{
|
||||
// // allow unknown names in metadata/style namespaces if needed, otherwise reject
|
||||
// // Here we reject unknown element names to be stricter
|
||||
// error = $"Disallowed or unknown SVG element: {localName}";
|
||||
// return false;
|
||||
//}
|
||||
//
|
||||
|
||||
if (reader.HasAttributes)
|
||||
{
|
||||
while (reader.MoveToNextAttribute())
|
||||
{
|
||||
var attrName = reader.Name ?? string.Empty;
|
||||
var attrValue = reader.Value ?? string.Empty;
|
||||
|
||||
// Disallow event handler attributes like onclick, onload, etc.
|
||||
if (attrName.StartsWith("on", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = $"Disallowed attribute: {attrName}";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disallow javascript: URIs
|
||||
if (attrValue.IndexOf("javascript:", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
error = $"Disallowed URI scheme in attribute {attrName}";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disallow external http/https references in href attributes
|
||||
if (attrName.EndsWith("href", StringComparison.OrdinalIgnoreCase) ||
|
||||
attrName.Equals("src", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var trimmedVal = attrValue.Trim();
|
||||
if (trimmedVal.StartsWith("http:", StringComparison.OrdinalIgnoreCase) ||
|
||||
trimmedVal.StartsWith("https:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = $"External references are not allowed ({attrName})";
|
||||
return false;
|
||||
}
|
||||
|
||||
// If data URIs are allowed for images, you could validate a whitelist like data:image/png;base64,...
|
||||
// For stricter policy, block all data: URIs:
|
||||
if (trimmedVal.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// reject data URIs to be conservative
|
||||
error = $"Embedded data URIs are not allowed ({attrName})";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: allow only known attributes; reject others to be stricter
|
||||
if (!AllowedAttributes.Contains(attrName))
|
||||
{
|
||||
// allow style attribute; already in whitelist. If attribute is namespaced (e.g., xml:space), allow commonly used ones:
|
||||
if (!attrName.Contains(":")) // very basic rule
|
||||
{
|
||||
error = $"Disallowed or unknown attribute: {attrName}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// move back to element
|
||||
reader.MoveToElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (XmlException xe)
|
||||
{
|
||||
error = $"Malformed XML: {xe.Message}";
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = $"SVG validation failed: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user