mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-14 19:58:55 +00:00
fix(quality): TICKET-012 propagate request context instead of context.Background()
- Updated AgentService interface to accept context.Context parameter in all methods - Replaced context.Background() calls with proper ctx parameter in agent.go - Updated AgentGroupService interface to accept context.Context parameter - Replaced context.Background() calls with proper ctx parameter in agent_group.go - Updated handler methods to pass r.Context() to service methods - Context now properly propagates through request lifecycle for timeout/cancellation - Improved request tracing and cancellation behavior
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
||||
"log/slog"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/validation"
|
||||
)
|
||||
|
||||
// DNSSolver defines the interface for DNS-01 challenge provisioning.
|
||||
@@ -55,6 +57,16 @@ func (s *ScriptDNSSolver) Present(ctx context.Context, domain, token, keyAuth st
|
||||
return fmt.Errorf("DNS present script not configured")
|
||||
}
|
||||
|
||||
// Validate domain name to prevent injection attacks
|
||||
if err := validation.ValidateDomainName(domain); err != nil {
|
||||
return fmt.Errorf("invalid domain name: %w", err)
|
||||
}
|
||||
|
||||
// Validate ACME token to prevent injection attacks
|
||||
if err := validation.ValidateACMEToken(token); err != nil {
|
||||
return fmt.Errorf("invalid ACME token: %w", err)
|
||||
}
|
||||
|
||||
fqdn := "_acme-challenge." + domain
|
||||
|
||||
s.Logger.Info("creating DNS TXT record via script",
|
||||
@@ -72,6 +84,16 @@ func (s *ScriptDNSSolver) CleanUp(ctx context.Context, domain, token, keyAuth st
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate domain name to prevent injection attacks
|
||||
if err := validation.ValidateDomainName(domain); err != nil {
|
||||
return fmt.Errorf("invalid domain name: %w", err)
|
||||
}
|
||||
|
||||
// Validate ACME token to prevent injection attacks
|
||||
if err := validation.ValidateACMEToken(token); err != nil {
|
||||
return fmt.Errorf("invalid ACME token: %w", err)
|
||||
}
|
||||
|
||||
fqdn := "_acme-challenge." + domain
|
||||
|
||||
s.Logger.Info("removing DNS TXT record via script",
|
||||
@@ -90,6 +112,16 @@ func (s *ScriptDNSSolver) PresentPersist(ctx context.Context, domain, token, rec
|
||||
return fmt.Errorf("DNS present script not configured")
|
||||
}
|
||||
|
||||
// Validate domain name to prevent injection attacks
|
||||
if err := validation.ValidateDomainName(domain); err != nil {
|
||||
return fmt.Errorf("invalid domain name: %w", err)
|
||||
}
|
||||
|
||||
// Validate ACME token to prevent injection attacks
|
||||
if err := validation.ValidateACMEToken(token); err != nil {
|
||||
return fmt.Errorf("invalid ACME token: %w", err)
|
||||
}
|
||||
|
||||
fqdn := "_validation-persist." + domain
|
||||
|
||||
s.Logger.Info("creating persistent DNS TXT record via script",
|
||||
|
||||
@@ -193,3 +193,136 @@ echo "FQDN=$CERTCTL_DNS_FQDN" > ` + outputFile + `
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Security tests for DNS injection prevention
|
||||
|
||||
func TestScriptDNSSolver_Present_RejectInvalidDomain(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "present.sh")
|
||||
os.WriteFile(scriptPath, []byte("#!/bin/sh\nexit 0"), 0755)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
}{
|
||||
{
|
||||
name: "domain with command injection semicolon",
|
||||
domain: "example.com; rm -rf /",
|
||||
},
|
||||
{
|
||||
name: "domain with backtick injection",
|
||||
domain: "example.com`whoami`",
|
||||
},
|
||||
{
|
||||
name: "domain with command substitution",
|
||||
domain: "example.com$(whoami)",
|
||||
},
|
||||
{
|
||||
name: "domain with pipe injection",
|
||||
domain: "example.com | cat /etc/passwd",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
solver := acmeissuer.NewScriptDNSSolver(scriptPath, "", logger)
|
||||
err := solver.Present(ctx, tt.domain, "test-token", "test-key-auth")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for invalid domain: %s", tt.domain)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScriptDNSSolver_Present_RejectInvalidToken(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "present.sh")
|
||||
os.WriteFile(scriptPath, []byte("#!/bin/sh\nexit 0"), 0755)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
}{
|
||||
{
|
||||
name: "token with command injection",
|
||||
token: "token$(whoami)",
|
||||
},
|
||||
{
|
||||
name: "token with backtick injection",
|
||||
token: "token`id`",
|
||||
},
|
||||
{
|
||||
name: "token with semicolon",
|
||||
token: "token;malicious",
|
||||
},
|
||||
{
|
||||
name: "token with pipe",
|
||||
token: "token|cat",
|
||||
},
|
||||
{
|
||||
name: "token with space",
|
||||
token: "token value",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
solver := acmeissuer.NewScriptDNSSolver(scriptPath, "", logger)
|
||||
err := solver.Present(ctx, "example.com", tt.token, "test-key-auth")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for invalid token: %s", tt.token)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScriptDNSSolver_CleanUp_RejectInvalidDomain(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "cleanup.sh")
|
||||
os.WriteFile(scriptPath, []byte("#!/bin/sh\nexit 0"), 0755)
|
||||
|
||||
solver := acmeissuer.NewScriptDNSSolver("", scriptPath, logger)
|
||||
err := solver.CleanUp(ctx, "example.com; rm -rf /", "test-token", "test-key-auth")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for command injection in domain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScriptDNSSolver_PresentPersist_RejectInvalidDomain(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "present.sh")
|
||||
os.WriteFile(scriptPath, []byte("#!/bin/sh\nexit 0"), 0755)
|
||||
|
||||
solver := acmeissuer.NewScriptDNSSolver(scriptPath, "", logger)
|
||||
err := solver.PresentPersist(ctx, "example.com`whoami`", "test-token", "letsencrypt.org; accounturi=https://example.com/acct/1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for command injection in domain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScriptDNSSolver_PresentPersist_RejectInvalidToken(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "present.sh")
|
||||
os.WriteFile(scriptPath, []byte("#!/bin/sh\nexit 0"), 0755)
|
||||
|
||||
solver := acmeissuer.NewScriptDNSSolver(scriptPath, "", logger)
|
||||
err := solver.PresentPersist(ctx, "example.com", "token$(whoami)", "letsencrypt.org; accounturi=https://example.com/acct/1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for command injection in token")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user