Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion cloudflare.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package cloudflare
import (
"fmt"
"regexp"
"strings"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
Expand Down Expand Up @@ -50,11 +51,21 @@ func (p *Provider) Provision(ctx caddy.Context) error {
p.Provider.APIToken = caddy.NewReplacer().ReplaceAll(p.Provider.APIToken, "")
p.Provider.ZoneToken = caddy.NewReplacer().ReplaceAll(p.Provider.ZoneToken, "")
if !validCloudflareToken(p.Provider.APIToken) {
return fmt.Errorf("API token '%s' appears invalid; ensure it's correctly entered and not wrapped in braces nor quotes", p.Provider.APIToken)
return fmt.Errorf("API token '%s' appears invalid; ensure it's correctly entered and not wrapped in braces nor quotes", redactToken(p.Provider.APIToken))
}
return nil
}

// redactToken returns a redacted version of the token, showing only the first 8
// and last 4 characters with the middle replaced by asterisks. This prevents
// accidental credential leakage in error messages and log output.
func redactToken(token string) string {
if len(token) <= 12 {
return strings.Repeat("*", len(token))
}
return token[:8] + strings.Repeat("*", len(token)-12) + token[len(token)-4:]
}

// validCloudflareToken returns true for legacy API tokens (35–50 chars) or cfut_/cfat_ tokens.
func validCloudflareToken(token string) bool {
return newCloudflareTokenRegexp.MatchString(token) || legacyCloudflareTokenRegexp.MatchString(token)
Expand Down
54 changes: 54 additions & 0 deletions cloudflare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,60 @@ func min(a, b int) int {
return b
}

func TestProvisionErrorRedactsToken(t *testing.T) {
// A token that contains characters outside [A-Za-z0-9_-] so it fails
// both regexes, but is long enough (>12 chars) to exercise redaction.
badToken := "this_is_not_a_valid_token!!"
p := Provider{&cloudflare.Provider{APIToken: badToken}}

err := p.Provision(caddy.Context{})
if err == nil {
t.Fatal("expected Provision to fail for an invalid token")
}

errMsg := err.Error()

// The full token must NOT appear anywhere in the error message.
if strings.Contains(errMsg, badToken) {
t.Errorf("error message contains the full unredacted token: %s", errMsg)
}

// The redacted form should be present (first 8 + last 4 visible).
redacted := redactToken(badToken)
if !strings.Contains(errMsg, redacted) {
t.Errorf("error message does not contain the redacted token %q: %s", redacted, errMsg)
}
}

func TestRedactToken(t *testing.T) {
tests := []struct {
name string
token string
expected string
}{
{"short token (<= 12 chars)", "abc", "***"},
{"exactly 12 chars", "123456789012", "************"},
{"13 chars shows first 8 + last 4", "1234567890123", "12345678*0123"},
{"legacy token", "Sqqty8-Vn0iOP29rvqYgwKz_xqGQ4y5JhuVL1-qU", "Sqqty8-V****************************1-qU"},
{"cfat_ token", "cfat_THIS_IS_A_FAKE_TOKEN_FOR_TESTING", "cfat_THI*************************TING"},
{"empty", "", ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := redactToken(tt.token)
if got != tt.expected {
t.Errorf("redactToken(%q) = %q, want %q", tt.token, got, tt.expected)
}
// Regardless of expected value, the full token must not survive redaction
// (unless the token is <= 12 chars, in which case it's fully masked).
if len(tt.token) > 12 && strings.Contains(got, tt.token) {
t.Errorf("redactToken(%q) still contains the full token", tt.token)
}
})
}
}

func TestValidToken(t *testing.T) {
goodToken := "Sqqty8-Vn0iOP29rvqYgwKz_xqGQ4y5JhuVL1-qU"
config := fmt.Sprintf(`cloudflare %s`, goodToken)
Expand Down