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:
shankar0123
2026-03-27 21:35:22 -04:00
parent 3e5cc86c5a
commit 200bdf990f
11 changed files with 413 additions and 81 deletions
+32
View File
@@ -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",
+133
View File
@@ -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")
}
}