Skip to content

Add PayPal REST API secret detector and validator. Fixes #1027 - #1815

Open
cybrdude wants to merge 44 commits into
google:mainfrom
cybrdude:paypal-secret-detector
Open

cybrdude wants to merge 44 commits into
google:mainfrom
cybrdude:paypal-secret-detector

Conversation

@cybrdude

Copy link
Copy Markdown

Secret name: PayPal REST API Client ID & Client Secret
Risk: Attackers can impersonate PayPal accounts, initiate transactions, access payment data
Validation: OAuth2 client_credentials flow against api-m.paypal.com and api-m.sandbox.paypal.com
Tests: Acceptance tests, true positive/negative cases, mock HTTP validation tests
Fixes #1027

Adds detection for PayPal Client ID and Client Secret with OAuth2
validation against both live and sandbox endpoints.

Fixes google#1027
@google-cla

google-cla Bot commented Feb 23, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@cybrdude

Copy link
Copy Markdown
Author

@erikvarga ready for QA/QC

@cybrdude
cybrdude marked this pull request as draft February 26, 2026 16:36
@cybrdude
cybrdude marked this pull request as ready for review February 26, 2026 16:42
@cybrdude

Copy link
Copy Markdown
Author

@erikvarga the lint-plugger failure is pre-existing from main and not related to the PayPal secret detector.

…ns in enricherlist

During merge of main into paypal-secret-detector, the following were
accidentally dropped from enricher/enricherlist/list.go:

- Import: veles/secrets/discordbottoken
- Import: veles/secrets/packagist
- 5x packagist fromVeles() validator registrations
- 1x discordbottoken fromVeles() validator registration

This caused the lint-plugger CI check to fail with:
  veles/secrets/discordbottoken/validator.go:35:1: plugger: veles.Validator is not registered
  veles/secrets/packagist/validator.go:37,56,149,156,163: plugger: veles.Validator is not registered

Restores all dropped lines to match google:main.
@cybrdude

Copy link
Copy Markdown
Author

@erikvarga fixed the lint errors. This feature is ready for review. Thank you.

@cybrdude

Copy link
Copy Markdown
Author

@another-rex @erikvarga PR is review-ready, lint-clean, all checks passing

@cybrdude

Copy link
Copy Markdown
Author

@erikvarga PR is ready for review - related to #1027 (comment)

@cybrdude

cybrdude commented Jul 7, 2026

Copy link
Copy Markdown
Author

Hi @cybrdude

Thanks for the updates and the documentation. I left a couple more comments to reduce the risk of the detector reporting false positives.

Once those are resolved, the PR looks good on my side.

Thank you @alessandro-Doyensec ,

Added word boundaries (\bA… / \bE…), matching the structural + word-boundary approach used elsewhere (e.g. gcpoauth2client's \bGOCSPX-…). On requiring a context keyword before the value: I prototyped it, but it breaks velestest.AcceptDetector, which asserts the secret is reported at the start of the embedded example a leading keyword shifts the position by its length. No pair detector in the tree uses a preceding context token. Between the word boundary and the pair requirement (an A- and an E-prefixed token within 200 bytes)

@alessandro-Doyensec

Copy link
Copy Markdown
Collaborator

Hi @cybrdude

Thanks again.

I prototyped it, but it breaks velestest.AcceptDetector, which asserts the secret is reported at the start of the embedded example a leading keyword shifts the position by its length

To solve this issue, it should be enough to report the position of the secret right at the start of the matched payload. So, for example:

  • context_key=A392rn23r23rk32rm32

    The position here should be 0

  • some header context_key=A392rn23r23rk32rm32

    The position here should be 12.

Hope I've been clear enough, feel free to ping me if you need more details.

@cybrdude

cybrdude commented Jul 9, 2026

Copy link
Copy Markdown
Author

@alessandro-Doyensec The detector now requires a PayPal/client-id (and client-secret) context keyword and reports the position at the start of the matched payload per your example (client_id=A… → 0; some header client_id=A… → the keyword offset). The credential stays capture group 1, so the reported secret value is clean. AcceptDetector's position cases pass, and I added a true-negative for a bare A…/E… pair with no keyword. Ready for another look.

@alessandro-Doyensec alessandro-Doyensec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @cybrdude

Thanks for the update.

Could you improve the regex logic to match these common context keys:

  • PAYPAL_ID
  • PAYPAL_CLIENT_ID
  • PAYPAL_SECRETKEY
  • PAYPAL_CLIENT_SECRET
  • PAYPAL_SECRET

(Note: some of these might already be catched by the current regexes, I added them for clarity)

Thanks in advance.

@cybrdude

cybrdude commented Jul 9, 2026

Copy link
Copy Markdown
Author

Thanks @alessandro-Doyensec . Completed.

All five keys are recognized now. On your note that some were already caught: PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET were already matched via the generic client_id / client_secret groups; PAYPAL_ID, PAYPAL_SECRET, and PAYPAL_SECRETKEY were previously missed because the _ after PAYPAL fell outside the separator class, so the paypal branch never reached the value. The keyword groups are now:

  • ID: (?i:paypal(?:[ _-]?client)?[ _-]?id|client[ _-]?id|paypal)
  • Secret: (?i:paypal(?:[ _-]?client)?[ _-]?secret(?:[ _-]?key)?|client[ _-]?secret|paypal)

These also cover space/hyphen/underscore and no-separator variants and any case. The credential stays capture group 1, the A/E prefix stays case-sensitive, and the pair-within-200-bytes requirement is unchanged, so false-positive exposure does not widen. I added table tests for all five keys plus JSON and mixed-case shapes, and two negatives (an unrelated key on the secret side, and a bare SECRETKEY with no paypal/client keyword) to lock the anti-FP behavior. gofmt clean, tests green.

@alessandro-Doyensec alessandro-Doyensec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @cybrdude

Thanks for the update. I just noticed that the validator logic does not use the common utility: simplevalidate.Validator. It should be fairly simple to plug it in.

Here's a draft of what the validator code should look like:

func NewValidator() *simplevalidate.Validator[Credentials] {
	return &simplevalidate.Validator[Credentials]{
		Endpoints: []string{paypalLiveTokenEndpoint, paypalSandboxTokenEndpoint},
		Body: func(c Credentials) (string, error) {
			return "grant_type=client_credentials", nil
		},
		HTTPMethod: http.MethodPost,
		HTTPHeaders: func(c Credentials) map[string]string {
			return map[string]string{
				"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte(c.ID+":"+c.Secret)),
				"Content-Type":  "application/x-www-form-urlencoded",
			}
		},
		ValidResponseCodes:   []int{http.StatusOK},
		InvalidResponseCodes: []int{http.StatusUnauthorized},
	}
}

Feel free to ping me if anything is unclear.

@cybrdude

Copy link
Copy Markdown
Author

@alessandro-Doyensec validator.go now uses simplevalidate.Validator[Credentials] per your draft (Live+Sandbox in Endpoints, 200 is Valid / 401 is Invalid / else = Failed). I kept the original 10s timeout via HTTPC (same as squareapikey)

@alessandro-Doyensec alessandro-Doyensec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @cybrdude

Thanks for the changes, the PR looks good to me now!

@alessandro-Doyensec alessandro-Doyensec added the lgtm The PR has been reviewed and approved ("Looks Good To Me") by vendors helping with code reviews. label Jul 13, 2026
@cybrdude

Copy link
Copy Markdown
Author

@alessandro-Doyensec , The UI lists that you only have read-only approve permissions. The restrictions indicate 'Merging is only allowed by At least 1 approving review is required by reviewers with write access.'

Resolves the SecretData.secret field-number collision: main added HTTPCookie http_cookie = 92, so paypal_credentials moves to 93. scan_result.pb.go regenerated with protoc from the merged proto.
@cybrdude

Copy link
Copy Markdown
Author

@erikvarga picking this back up.

Current status: approved by @alessandro-Doyensec on Jul 1, merge pending. main had moved, so I merged it in today as 2539ca5:

  • binary/proto/scan_result.proto: main took field 92 in SecretData.secret for HTTPCookie http_cookie, so PayPalCredentials paypal_credentials moves from 92 to 93, the next free number (the message only reserves 3).
  • binary/proto/scan_result_go_proto/scan_result.pb.go: regenerated from the merged proto with protoc 23.x and protoc-gen-go v1.36.11, the same versions the Go workflow installs, rather than hand-merged.

The branch builds, the proto, paypal, and list tests pass, and no conflicts remain. cla/google is green on the new head.

Asks:
The three workflow runs on 2539ca5 are sitting at "awaiting approval", so the required GitHub Actions Scan check cannot report until a maintainer approves them.

Regenerated with the toolchain recorded in main (protoc 3.21.12, protoc-gen-go 1.36.11) from scan_result.proto merged with main, so the type index includes LayerAttribute from main and PayPalCredentials from this branch. Build and tests pass on Go 1.26.
@cybrdude

cybrdude commented Oct 7, 2026

Copy link
Copy Markdown
Author

@alessandro-Doyensec @erikvarga is there anything else needed from my end?

@cybrdude

Copy link
Copy Markdown
Author

@alessandro-Doyensec @erikvarga Merged with main again on Oct 25 (8ee4b70) and regenerated scan_result.pb.go with the repo's protoc / protoc-gen-go versions; the build and the paypal, binary/proto and list tests pass. Is anything else needed before this can merge?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm The PR has been reviewed and approved ("Looks Good To Me") by vendors helping with code reviews.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PRP: Secret extractor for PayPal

2 participants