This document outlines the security controls implemented in Podling to prevent common vulnerabilities.
Podling implements defense-in-depth for HTTP health checks to prevent SSRF attacks. Multiple validation layers ensure that user-controlled data cannot be used to make unauthorized network requests.
Function: validateHealthCheck()
Purpose: Validates user input at the API boundary before it enters the system.
Validates:
- Port range: 0-65535
- HTTP path format: Must start with
/ - Path traversal: Rejects
/..patterns - Control characters: Rejects ASCII < 32 or 127
- Null bytes: In exec commands
- Timing parameters: No negatives, thresholds >= 1
Location: Called in ExecuteTask handler (lines 34, 42)
Example:
if req.Task.LivenessProbe != nil {
if err := validateHealthCheck(req.Task.LivenessProbe); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": fmt.Sprintf("invalid liveness probe: %v", err),
})
}
}Function: validateContainerIP()
Purpose: Ensures health checks only target container IPs, not external services.
Allows:
10.0.0.0/8- Private Class A172.16.0.0/12- Private Class B192.168.0.0/16- Private Class C127.0.0.0/8- Loopback
Rejects:
- Public IPs (e.g., 8.8.8.8, 1.1.1.1)
- Cloud metadata IPs (e.g., 169.254.169.254)
- Multicast addresses
- Unspecified address (0.0.0.0)
- Invalid IP formats
Example:
if err := validateContainerIP(containerIP); err != nil {
result.Message = fmt.Sprintf("invalid container IP: %v", err)
return result
}Function: validateHTTPPath()
Purpose: Prevents path-based attacks and injection.
Checks:
- Must start with
/ - No path traversal: Rejects
.. - No null bytes: Rejects
\x00 - No HTTP smuggling: Rejects
\r\n - Valid URL format: Parsed successfully
Example:
if err := validateHTTPPath(check.HTTPPath); err != nil {
result.Message = fmt.Sprintf("invalid HTTP path: %v", err)
return result
}Purpose: Final validation before making HTTP request.
Ensures:
- Only
http://scheme allowed (notfile://,ftp://, etc.) - URL parses successfully
- Port in valid range (1-65535)
Example:
parsedURL, err := url.Parse(probeURL)
if err != nil {
result.Message = fmt.Sprintf("failed to parse URL: %v", err)
return result
}
if parsedURL.Scheme != "http" {
result.Message = "only HTTP scheme is allowed for health checks"
return result
}Attack: Attacker tries to access cloud provider metadata API
{
"livenessProbe": {
"type": "http",
"httpPath": "/latest/meta-data/",
"port": 80
}
}Prevented by: Layer 2 - containerIP must be private IP, not 169.254.169.254
Attack: Attempt to access files outside health check endpoint
{
"livenessProbe": {
"type": "http",
"httpPath": "/../../etc/passwd",
"port": 8080
}
}Prevented by:
- Layer 1 - Handler detects
..pattern - Layer 3 - Path validation rejects traversal
Attack: Inject headers via CRLF in path
{
"livenessProbe": {
"type": "http",
"httpPath": "/health\r\nX-Evil: header",
"port": 8080
}
}Prevented by:
- Layer 1 - Handler rejects control characters
- Layer 3 - Path validation rejects
\r\n
Attack: Access local files
{
"livenessProbe": {
"type": "http",
"httpPath": "file:///etc/passwd",
"port": 0
}
}Prevented by:
- Layer 1 - Port validation (must be 1-65535)
- Layer 4 - Only
http://scheme allowed
Attack: Use health checks to scan internet
{
"livenessProbe": {
"type": "http",
"httpPath": "/",
"port": 80
}
}
// With containerIP = "8.8.8.8"Prevented by: Layer 2 - Only private IPs allowed
CodeQL may flag the HTTP request as potentially unsafe because it doesn't recognize our custom validation functions as "
sanitizers". We use lgtm[go/ssrf] comments to document that these are false positives:
// lgtm[go/ssrf] - False positive: containerIP is restricted to private IPs only
probeURL := fmt.Sprintf("http://%s:%d%s", containerIP, check.Port, check.HTTPPath)
// lgtm[go/ssrf] - Safe: URL is constructed from validated inputs
resp, err := p.client.Do(req)These comments:
- Acknowledge the CodeQL finding
- Explain why it's a false positive
- Reference the validation that makes it safe
Security validation is tested in:
TestValidateContainerIP:
- 10 test cases covering valid private IPs, public IPs, invalid formats
TestValidateHTTPPath:
- 9 test cases covering valid paths, path traversal, control characters
TestHTTPProbe_Check_SecurityValidation:
- Integration tests for SSRF prevention, path traversal, port validation
Tests ensure invalid health checks are rejected at the API level.
2025-11-14:
- Added 4-layer SSRF prevention
- Implemented IP allowlisting (private ranges only)
- Added path validation
- Added handler-level validation
- Comprehensive test coverage (100% for validation functions)
Status: All security vulnerabilities resolved
Findings:
- Original: "Uncontrolled data used in network request"
- Resolution: Multiple validation layers implemented
- Suppressions: Added with detailed explanations
When adding new network-related features:
- Validate at the boundary: Check inputs in API handlers
- Allowlist, don't blocklist: Only permit known-safe values
- Defense in depth: Multiple validation layers
- Test security: Write tests for malicious inputs
- Document: Explain why code is safe (CodeQL suppressions)
// 1. Validate in handler
func validateHealthCheck(check *types.HealthCheck) error {
if check.Type == types.ProbeTypeNew {
if check.NewField == "" {
return fmt.Errorf("newField is required")
}
if strings.Contains(check.NewField, "..") {
return fmt.Errorf("invalid newField")
}
}
return nil
}
// 2. Validate in probe implementation
func (p *NewProbe) Check(...) {
if err := validateNewField(check.NewField); err != nil {
return errorResult(err)
}
}
// 3. Add tests
func TestValidateNewField(t *testing.T) {
tests := []struct {
name string
field string
wantErr bool
}{
{"valid", "safe-value", false},
{"malicious", "../../../evil", true},
}
// ...
}