From ce7b2b838ceb4a90930c15cddd3848731604f5df Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 14 Sep 2026 14:16:40 -0400 Subject: [PATCH] fix(detectors/ngrok): broaden valid bearer tokens matching (#5152) * fix(detectors/ngrok): match API keys that do not start with 2 The ngrok bearer token pattern is 27 alphanumerics, an underscore, and 21 alphanumerics. Requiring a leading 2 missed valid API keys and ak_ resource IDs were never secrets. Co-authored-by: Cursor * fix(detectors/ngrok): restore digit-leading suffix constraint The prefix broadening accidentally allowed any alphanumeric suffix start, which would increase false positives. Co-authored-by: Cursor * fix(detectors/ngrok): accept 20-21 char suffixes with any leading char Real authtokens verified against the ngrok API can have 20-char and letter-leading suffixes, so the digit-leading 21-char suffix constraint drops valid secrets. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- pkg/detectors/ngrok/ngrok.go | 8 +++++++- pkg/detectors/ngrok/ngrok_test.go | 32 ++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/pkg/detectors/ngrok/ngrok.go b/pkg/detectors/ngrok/ngrok.go index ede1b6eb8..0ff517fc0 100644 --- a/pkg/detectors/ngrok/ngrok.go +++ b/pkg/detectors/ngrok/ngrok.go @@ -38,7 +38,13 @@ const ( ) var ( - keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ngrok"}) + `\b(2[a-zA-Z0-9]{26}_\d[a-zA-Z0-9]{20})\b`) + // ngrok API keys and authtokens are {prefix}_{suffix}, both alphanumeric with + // no fixed leading character. The prefix is 27 chars. Suffixes are usually 21 + // chars with a leading digit, but real authtokens verified against the ngrok + // API (via ERR_NGROK_206) have been observed with 20-char and letter-leading + // suffixes, so the pattern must accept 20-21 chars starting with any + // alphanumeric character. + keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ngrok"}) + `\b([a-zA-Z0-9]{27}_[a-zA-Z0-9]{20,21})\b`) ) func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { diff --git a/pkg/detectors/ngrok/ngrok_test.go b/pkg/detectors/ngrok/ngrok_test.go index 033b56ed1..71d3da08f 100644 --- a/pkg/detectors/ngrok/ngrok_test.go +++ b/pkg/detectors/ngrok/ngrok_test.go @@ -12,9 +12,15 @@ import ( ) var ( - validPattern = "2WIRSIHOQyHSVklZnoz2k6bTYdH_0E7z0Ta9QEyR1fZvQ0KU9" - invalidPattern = "2WIRSIHOQyHSVklZnoz2k6bT?dH_0E7z0Ta9QEyR1fZvQ0KU9" - keyword = "ngrok" + validPattern = "2WIRSIHOQyHSVklZnoz2k6bTYdH_0E7z0Ta9QEyR1fZvQ0KU9" + validPatternAlt = "3GuTI5JXfaLtYILkc6N0xopP2V0_89zkDfefiuMv6Hk1z6oYi" + // Authtokens exist in the wild with letter-leading and 20-char suffixes + // (confirmed live against the ngrok API), so both must be detected. + validPatternLetterSuffix = "3GuTI5JXfaLtYILkc6N0xopP2V0_a9zkDfefiuMv6Hk1z6oYi" + validPatternShortSuffix = "4HvUJ6KYgbMuZJMld7O1yqqQ3W1_b8ylEgfgjvNw7Il2z7pZ" + invalidPattern = "2WIRSIHOQyHSVklZnoz2k6bT?dH_0E7z0Ta9QEyR1fZvQ0KU9" + invalidIDPattern = "ak_3GuTI5JXfaLtYILkc6N0xopP2V0" + keyword = "ngrok" ) func TestNgrok_Pattern(t *testing.T) { @@ -30,11 +36,26 @@ func TestNgrok_Pattern(t *testing.T) { input: fmt.Sprintf("%s token = '%s'", keyword, validPattern), want: []string{"2WIRSIHOQyHSVklZnoz2k6bTYdH_0E7z0Ta9QEyR1fZvQ0KU9"}, }, + { + name: "valid pattern - api key not starting with 2", + input: fmt.Sprintf("%s token = '%s'", keyword, validPatternAlt), + want: []string{validPatternAlt}, + }, { name: "valid pattern - ignore duplicate", input: fmt.Sprintf("%s token = '%s' | '%s'", keyword, validPattern, validPattern), want: []string{"2WIRSIHOQyHSVklZnoz2k6bTYdH_0E7z0Ta9QEyR1fZvQ0KU9"}, }, + { + name: "valid pattern - authtoken suffix not starting with digit", + input: fmt.Sprintf("%s token = '%s'", keyword, validPatternLetterSuffix), + want: []string{validPatternLetterSuffix}, + }, + { + name: "valid pattern - authtoken with 20 char suffix", + input: fmt.Sprintf("%s token = '%s'", keyword, validPatternShortSuffix), + want: []string{validPatternShortSuffix}, + }, { name: "valid pattern - key out of prefix range", input: fmt.Sprintf("%s keyword is not close to the real key in the data\n = '%s'", keyword, validPattern), @@ -45,6 +66,11 @@ func TestNgrok_Pattern(t *testing.T) { input: fmt.Sprintf("%s = '%s'", keyword, invalidPattern), want: []string{}, }, + { + name: "invalid pattern - api key resource id not bearer token", + input: fmt.Sprintf("%s api_key = '%s'", keyword, invalidIDPattern), + want: []string{}, + }, } for _, test := range tests {