Skip to content

Security: danpasecinic/podling

Security

docs/SECURITY.md

Security Measures in Podling

This document outlines the security controls implemented in Podling to prevent common vulnerabilities.

SSRF (Server-Side Request Forgery) Prevention

Overview

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.

Validation Layers

Layer 1: API Handler Validation (internal/worker/agent/handlers.go)

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),
        })
    }
}

Layer 2: Container IP Validation (internal/worker/health/http.go)

Function: validateContainerIP()

Purpose: Ensures health checks only target container IPs, not external services.

Allows:

  • 10.0.0.0/8 - Private Class A
  • 172.16.0.0/12 - Private Class B
  • 192.168.0.0/16 - Private Class C
  • 127.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
}

Layer 3: HTTP Path Validation (internal/worker/health/http.go)

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
}

Layer 4: URL Scheme Validation (internal/worker/health/http.go)

Purpose: Final validation before making HTTP request.

Ensures:

  • Only http:// scheme allowed (not file://, 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 Scenarios Prevented

1. SSRF to Cloud Metadata

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


2. Path Traversal

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

3. HTTP Smuggling

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

4. File URI Scheme

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

5. Public IP Scanning

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 Suppressions

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:

  1. Acknowledge the CodeQL finding
  2. Explain why it's a false positive
  3. Reference the validation that makes it safe

Testing

Security validation is tested in:

HTTP Health Check Tests (internal/worker/health/http_test.go)

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

Handler Tests (internal/worker/agent/handlers_test.go)

Tests ensure invalid health checks are rejected at the API level.


Audit Trail

Changes

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)

CodeQL Analysis

Status: All security vulnerabilities resolved

Findings:

  • Original: "Uncontrolled data used in network request"
  • Resolution: Multiple validation layers implemented
  • Suppressions: Added with detailed explanations

Best Practices for Contributors

When adding new network-related features:

  1. Validate at the boundary: Check inputs in API handlers
  2. Allowlist, don't blocklist: Only permit known-safe values
  3. Defense in depth: Multiple validation layers
  4. Test security: Write tests for malicious inputs
  5. Document: Explain why code is safe (CodeQL suppressions)

Example: Adding a New Probe Type

// 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},
    }
    // ...
}

References

There aren’t any published security advisories