diff --git a/pkg/detectors/cloudflarecakey/cloudflarecakey.go b/pkg/detectors/cloudflarecakey/cloudflarecakey.go index d74c59d15..dfe21b52e 100644 --- a/pkg/detectors/cloudflarecakey/cloudflarecakey.go +++ b/pkg/detectors/cloudflarecakey/cloudflarecakey.go @@ -2,8 +2,9 @@ package cloudflarecakey import ( "context" + "fmt" + "io" "net/http" - "strings" regexp "github.com/wasilibs/go-re2" @@ -20,7 +21,8 @@ var _ detectors.Detector = (*Scanner)(nil) var ( client = common.SaneHttpClient() - keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloudflare"}) + `\b(v[A-Za-z0-9._-]{173,})\b`) + // origin ca keys documentation: https://developers.cloudflare.com/fundamentals/api/get-started/ca-keys/ + keyPat = regexp.MustCompile(`\b(v1\.0-[A-Za-z0-9-]{171})\b`) ) // Keywords are used for efficiently pre-filtering chunks. @@ -33,35 +35,22 @@ func (s Scanner) Keywords() []string { func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { dataStr := string(data) - matches := keyPat.FindAllStringSubmatch(dataStr, -1) + uniqueMatches := make(map[string]struct{}) - for _, match := range matches { - if len(match) != 2 { - continue - } - resMatch := strings.TrimSpace(match[1]) + for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) { + uniqueMatches[matches[1]] = struct{}{} + } + for caKey := range uniqueMatches { s1 := detectors.Result{ DetectorType: detectorspb.DetectorType_CloudflareCaKey, - Raw: []byte(resMatch), + Raw: []byte(caKey), } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudflare.com/client/v4/certificates?zone_id=a", nil) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("user-agent", "curl/7.68.0") // pretend to be from curl so we do not wait 100+ seconds -> nice try did not work - - req.Header.Add("X-Auth-User-Service-Key", resMatch) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyCloudFlareCAKey(ctx, client, caKey) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, caKey) } results = append(results, s1) @@ -77,3 +66,33 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Cloudflare is a web infrastructure and website security company. Cloudflare CA keys can be used to manage SSL/TLS certificates and other security settings." } + +func verifyCloudFlareCAKey(ctx context.Context, client *http.Client, caKey string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudflare.com/client/v4/certificates?zone_id=a", nil) + if err != nil { + return false, nil + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("user-agent", "curl/7.68.0") // pretend to be from curl so we do not wait 100+ seconds -> nice try did not work + + req.Header.Add("X-Auth-User-Service-Key", caKey) + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized, http.StatusForbidden: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/cloudflarecakey/cloudflarecakey_test.go b/pkg/detectors/cloudflarecakey/cloudflarecakey_test.go index 3e1a45ff2..450fc6258 100644 --- a/pkg/detectors/cloudflarecakey/cloudflarecakey_test.go +++ b/pkg/detectors/cloudflarecakey/cloudflarecakey_test.go @@ -21,14 +21,14 @@ var ( api: auth_type: "API-Key" - base_url: "https://api.example.com/v1/user" - cloudflare: "vR2hGdmAyzLxI4kR34v-fgtaIug7VC_xudRuku6UQd5t9RoiY3zGiuVxMGiN6qR6O0cGjbGepFsHHrfm-9QmBahS.tvHRjYvf_c5XOKReWeVXsp3T-_gilqReVlS-U_9vj1tcMAwRjWCcehKiCOPnXEeN.Xg.lmwp0.mAIsgbrdPJsJE4hz7OF5PQ7D" + base_url: "https://api.cloudflare.com/v1/user" + ca_key: "v1.0-13vvv5141b975834504fc75b-a670d21e1e012816c3c8d9745e2693adc2d2ec7c402f607dbf7f2bd5de3bdb490cce4420ef13179957c5651e1ee5d952b1e03bd0271e2b43a9847f0713f4d3942cde4a7bc2e4770615" # Notes: # - Remember to rotate the secret every 90 days. # - The above credentials should only be used in a secure environment. ` - secret = "vR2hGdmAyzLxI4kR34v-fgtaIug7VC_xudRuku6UQd5t9RoiY3zGiuVxMGiN6qR6O0cGjbGepFsHHrfm-9QmBahS.tvHRjYvf_c5XOKReWeVXsp3T-_gilqReVlS-U_9vj1tcMAwRjWCcehKiCOPnXEeN.Xg.lmwp0.mAIsgbrdPJsJE4hz7OF5PQ7D" + secret = "v1.0-13vvv5141b975834504fc75b-a670d21e1e012816c3c8d9745e2693adc2d2ec7c402f607dbf7f2bd5de3bdb490cce4420ef13179957c5651e1ee5d952b1e03bd0271e2b43a9847f0713f4d3942cde4a7bc2e4770615" ) func TestCloudFlareCAKey_Pattern(t *testing.T) {