mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 13:51:36 +00:00
fix(agent,service): SEC-002 — validate certificate_id shape + contain key path
Sprint 1 unified-master-audit closure. Pre-fix the agent built its
on-disk key path via:
keyPath := filepath.Join(a.config.KeyDir, job.CertificateID+".key")
migrations/000001_initial_schema.up.sql declares managed_certificates.id
as TEXT PRIMARY KEY with no shape constraint, so a compromised control
plane (or a poisoned database row) could deliver a job whose
certificate_id is '../../etc/passwd', '/absolute/path', a NUL-byte
payload, or a Windows-separator-laden string — driving arbitrary
file write or read on the agent host.
Fix (two ends; both load-bearing):
Server side:
- New internal/validation/certificate_id.go: ValidateCertificateID
pins the canonical TEXT-PK shape (^[A-Za-z0-9._-]{1,128}$, plus
explicit '.'/'..' rejection).
- CertificateService.Create now invokes ValidateCertificateID after
the existing required-fields check; malformed IDs are refused
before persistence or downstream job creation.
Agent side:
- cmd/agent/keymem.go: validateAgentCertID mirrors the server-side
shape regex. safeAgentKeyPath additionally asserts the joined
path is contained within KeyDir via filepath.Rel — even if a
future refactor bypasses the shape check, a path that escapes
KeyDir fails closed.
- poll.go + deploy.go: both filepath.Join call sites routed
through safeAgentKeyPath; rejection surfaces via reportJobStatus
so the control plane sees the failure.
Regression coverage:
- internal/validation/certificate_id_test.go: production shapes
accepted; explicit rejection table for empty, overlong, posix
traversal, absolute, Windows traversal, Windows separator, NUL
byte, newline/tab injection, drive prefix, space, unicode dots.
- cmd/agent/keymem_test.go: validateAgentCertID acceptance +
rejection tables; safeAgentKeyPath happy path + the 8 audit
vectors plus empty-keyDir refusal.
Closes SEC-002.
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/certctl-io/certctl/internal/domain"
|
||||
"github.com/certctl-io/certctl/internal/repository"
|
||||
"github.com/certctl-io/certctl/internal/validation"
|
||||
)
|
||||
|
||||
// CertificateService provides business logic for certificate management.
|
||||
@@ -163,6 +164,15 @@ func (s *CertificateService) Create(ctx context.Context, cert *domain.ManagedCer
|
||||
if cert.ID == "" || cert.CommonName == "" || cert.IssuerID == "" {
|
||||
return fmt.Errorf("invalid certificate: missing required fields")
|
||||
}
|
||||
// SEC-002 closure (Sprint 1, 2026-05-16): pin the certificate_id
|
||||
// shape at the server boundary. The agent derives an on-disk key
|
||||
// path from this ID via filepath.Join; without this gate a
|
||||
// crafted ID like "../../etc/passwd" or "/absolute/path" would
|
||||
// drive arbitrary file write/read on the agent host. Companion
|
||||
// containment check lives in cmd/agent/keymem.go (safeAgentKeyPath).
|
||||
if err := validation.ValidateCertificateID(cert.ID); err != nil {
|
||||
return fmt.Errorf("invalid certificate id: %w", err)
|
||||
}
|
||||
|
||||
// Run policy validation
|
||||
violations, err := s.policyService.ValidateCertificate(ctx, cert)
|
||||
|
||||
Reference in New Issue
Block a user