Files
go-jdenticon/internal/renderer/svg_security_test.go
Kevin McIntyre f1544ef49c
Some checks failed
CI / Test (Go 1.24.x, ubuntu-latest) (push) Successful in 1m53s
CI / Code Quality (push) Failing after 26s
CI / Security Scan (push) Failing after 11s
CI / Test Coverage (push) Successful in 1m13s
CI / Benchmarks (push) Failing after 10m22s
CI / Build CLI (push) Failing after 8s
Benchmarks / Run Benchmarks (push) Failing after 10m13s
Release / Test (push) Successful in 55s
Release / Build (amd64, darwin, ) (push) Failing after 12s
Release / Build (amd64, linux, ) (push) Failing after 6s
Release / Build (amd64, windows, .exe) (push) Failing after 12s
Release / Build (arm64, darwin, ) (push) Failing after 12s
Release / Build (arm64, linux, ) (push) Failing after 12s
Release / Release (push) Has been skipped
CI / Test (Go 1.24.x, macos-latest) (push) Has been cancelled
CI / Test (Go 1.24.x, windows-latest) (push) Has been cancelled
chore: update module path to gitea.dockr.co/kev/go-jdenticon
Move hosting from GitHub to private Gitea instance.
2026-02-10 10:07:57 -05:00

285 lines
8.6 KiB
Go

package renderer
import (
"strings"
"testing"
"gitea.dockr.co/kev/go-jdenticon/internal/engine"
)
// TestSVGRenderer_SecurityValidation tests defense-in-depth color validation
// This test addresses SEC-06 from the security report by verifying that
// the SVG renderer properly validates color inputs and prevents injection attacks.
func TestSVGRenderer_SecurityValidation(t *testing.T) {
tests := []struct {
name string
color string
expectInSVG bool
description string
}{
{
name: "valid_hex_color_3_digit",
color: "#f00",
expectInSVG: true,
description: "Valid 3-digit hex color should be rendered",
},
{
name: "valid_hex_color_6_digit",
color: "#ff0000",
expectInSVG: true,
description: "Valid 6-digit hex color should be rendered",
},
{
name: "valid_hex_color_8_digit",
color: "#ff0000ff",
expectInSVG: true,
description: "Valid 8-digit hex color with alpha should be rendered",
},
{
name: "injection_attempt_script",
color: "\"><script>alert('xss')</script><path fill=\"#000",
expectInSVG: false,
description: "Script injection attempt should be blocked",
},
{
name: "injection_attempt_svg_element",
color: "#f00\"/><use href=\"#malicious\"/><path fill=\"#000",
expectInSVG: false,
description: "SVG element injection attempt should be blocked",
},
{
name: "malformed_hex_no_hash",
color: "ff0000",
expectInSVG: false,
description: "Hex color without # should be rejected",
},
{
name: "valid_hex_color_4_digit_rgba",
color: "#ff00",
expectInSVG: true,
description: "Valid 4-digit RGBA hex color should be rendered",
},
{
name: "malformed_hex_invalid_length_5",
color: "#ff000",
expectInSVG: false,
description: "Invalid 5-character hex color should be rejected",
},
{
name: "malformed_hex_invalid_chars",
color: "#gggggg",
expectInSVG: false,
description: "Invalid hex characters should be rejected",
},
{
name: "empty_color",
color: "",
expectInSVG: false,
description: "Empty color string should be rejected",
},
{
name: "xml_entity_injection",
color: "#ff0000&lt;script&gt;",
expectInSVG: false,
description: "XML entity injection attempt should be blocked",
},
{
name: "path_data_injection",
color: "#f00\" d=\"M0 0L100 100Z\"/><script>alert('xss')</script><path fill=\"#000",
expectInSVG: false,
description: "Path data injection attempt should be blocked",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
renderer := NewSVGRenderer(100)
// Test BeginShape validation
renderer.BeginShape(tt.color)
// Add some path data to ensure the color would be rendered if valid
points := []engine.Point{
{X: 10, Y: 10},
{X: 50, Y: 10},
{X: 50, Y: 50},
{X: 10, Y: 50},
}
renderer.AddPolygon(points)
renderer.EndShape()
// Generate SVG output
svgOutput := renderer.ToSVG()
if tt.expectInSVG {
// Verify valid colors are present in the output
if !strings.Contains(svgOutput, `fill="`+tt.color+`"`) {
t.Errorf("Expected valid color %s to be present in SVG output, but it was not found.\nSVG: %s", tt.color, svgOutput)
}
// Ensure the path element is present for valid colors
if !strings.Contains(svgOutput, "<path") {
t.Errorf("Expected path element to be present for valid color %s, but it was not found", tt.color)
}
} else {
// Verify invalid/malicious colors are NOT present in the output
// Special handling for empty string since it's always "contained" in any string
if tt.color != "" && strings.Contains(svgOutput, tt.color) {
t.Errorf("Expected invalid/malicious color %s to be rejected, but it was found in SVG output.\nSVG: %s", tt.color, svgOutput)
}
// For invalid colors, no path should be rendered with that color
if strings.Contains(svgOutput, `fill="`+tt.color+`"`) {
t.Errorf("Expected invalid color %s to be rejected from fill attribute, but it was found", tt.color)
}
}
// Verify the SVG is still well-formed XML
if !strings.HasPrefix(svgOutput, "<svg") {
t.Errorf("SVG output should start with <svg tag")
}
if !strings.HasSuffix(svgOutput, "</svg>") {
t.Errorf("SVG output should end with </svg> tag")
}
})
}
}
// TestSVGRenderer_BackgroundColorValidation tests background color validation
func TestSVGRenderer_BackgroundColorValidation(t *testing.T) {
tests := []struct {
name string
bgColor string
opacity float64
expectInSVG bool
description string
}{
{
name: "valid_background_color",
bgColor: "#ffffff",
opacity: 1.0,
expectInSVG: true,
description: "Valid background color should be rendered",
},
{
name: "invalid_background_injection",
bgColor: "#fff\"/><script>alert('bg')</script><rect fill=\"#000",
opacity: 1.0,
expectInSVG: false,
description: "Background color injection should be blocked",
},
{
name: "malformed_background_color",
bgColor: "not-a-color",
opacity: 1.0,
expectInSVG: false,
description: "Invalid background color format should be rejected",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
renderer := NewSVGRenderer(100)
renderer.SetBackground(tt.bgColor, tt.opacity)
svgOutput := renderer.ToSVG()
if tt.expectInSVG {
// Verify valid background colors are present
if !strings.Contains(svgOutput, `<rect`) {
t.Errorf("Expected background rectangle to be present for valid color %s", tt.bgColor)
}
if !strings.Contains(svgOutput, `fill="`+tt.bgColor+`"`) {
t.Errorf("Expected valid background color %s to be present in SVG", tt.bgColor)
}
} else {
// Verify invalid background colors are rejected
if strings.Contains(svgOutput, tt.bgColor) {
t.Errorf("Expected invalid background color %s to be rejected, but found in: %s", tt.bgColor, svgOutput)
}
}
})
}
}
// TestSVGRenderer_MultipleInvalidColors tests behavior with multiple invalid colors
func TestSVGRenderer_MultipleInvalidColors(t *testing.T) {
renderer := NewSVGRenderer(100)
maliciousColors := []string{
"\"><script>alert(1)</script><path fill=\"#000",
"#invalid-color",
"javascript:alert('xss')",
"#ff0000\"/><use href=\"#malicious\"/>",
}
// Try to add shapes with all malicious colors
for _, color := range maliciousColors {
renderer.BeginShape(color)
points := []engine.Point{{X: 0, Y: 0}, {X: 50, Y: 50}}
renderer.AddPolygon(points)
renderer.EndShape()
}
svgOutput := renderer.ToSVG()
// Verify none of the malicious colors appear in the output
for _, color := range maliciousColors {
if strings.Contains(svgOutput, color) {
t.Errorf("Malicious color %s should not appear in SVG output, but was found: %s", color, svgOutput)
}
}
// Verify the SVG is still valid and doesn't contain path elements for rejected colors
pathCount := strings.Count(svgOutput, "<path")
if pathCount > 0 {
t.Errorf("Expected no path elements for invalid colors, but found %d", pathCount)
}
// Ensure SVG structure is intact
if !strings.Contains(svgOutput, `<svg xmlns="http://www.w3.org/2000/svg"`) {
t.Errorf("SVG should still have proper structure even with all invalid colors")
}
}
// TestSVGRenderer_ValidAndInvalidColorMix tests mixed valid/invalid colors
func TestSVGRenderer_ValidAndInvalidColorMix(t *testing.T) {
renderer := NewSVGRenderer(100)
// Add valid color
renderer.BeginShape("#ff0000")
renderer.AddPolygon([]engine.Point{{X: 0, Y: 0}, {X: 25, Y: 25}})
renderer.EndShape()
// Add invalid color
renderer.BeginShape("\"><script>alert('xss')</script><path fill=\"#000")
renderer.AddPolygon([]engine.Point{{X: 25, Y: 25}, {X: 50, Y: 50}})
renderer.EndShape()
// Add another valid color
renderer.BeginShape("#00ff00")
renderer.AddPolygon([]engine.Point{{X: 50, Y: 50}, {X: 75, Y: 75}})
renderer.EndShape()
svgOutput := renderer.ToSVG()
// Valid colors should be present
if !strings.Contains(svgOutput, `fill="#ff0000"`) {
t.Errorf("Valid color #ff0000 should be present in output")
}
if !strings.Contains(svgOutput, `fill="#00ff00"`) {
t.Errorf("Valid color #00ff00 should be present in output")
}
// Invalid color should be rejected
if strings.Contains(svgOutput, "script") {
t.Errorf("Invalid color with script injection should be rejected")
}
// Should have exactly 2 path elements (for the 2 valid colors)
pathCount := strings.Count(svgOutput, "<path")
if pathCount != 2 {
t.Errorf("Expected exactly 2 path elements for valid colors, got %d", pathCount)
}
}