diff --git a/Submission.Api/Controllers/SignController.cs b/Submission.Api/Controllers/SignController.cs index 8e81249..677cf95 100644 --- a/Submission.Api/Controllers/SignController.cs +++ b/Submission.Api/Controllers/SignController.cs @@ -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 diff --git a/Submission.Api/Services/SvgValidator.cs b/Submission.Api/Services/SvgValidator.cs new file mode 100644 index 0000000..ef6420f --- /dev/null +++ b/Submission.Api/Services/SvgValidator.cs @@ -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 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 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(" 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; + } + } +} \ No newline at end of file