Skip to content

Fix OAuth token-endpoint injection in the Adobe Sign and Zoho CRM samples - #562

Open
mohammad5525 wants to merge 1 commit into
googleworkspace:mainfrom
mohammad5525:security/validate-token-endpoint-params
Open

Fix OAuth token-endpoint injection in the Adobe Sign and Zoho CRM samples#562
mohammad5525 wants to merge 1 commit into
googleworkspace:mainfrom
mohammad5525:security/validate-token-endpoint-params

Conversation

@mohammad5525

Copy link
Copy Markdown

What this fixes

Both samples read a host/URL from an OAuth callback query parameter (api_access_point in samples/AdobeSign.gs, accounts-server in samples/ZohoCRM.gs) and pass it straight into setTokenUrl(), then persist it into the service storage.

In a web-app or add-on deployment the callback URL is reachable with attacker-chosen parameters, so a single crafted GET (no valid state required — Service_.handleCallback never checks state) makes Google's Apps Script server POST the token exchange — code, client_id, client_secret — to the attacker's host. The value is then persisted, so every later token refresh is also delivered to the attacker. In the Adobe Sign sample, the second setPropertyStore call moves the stored state into shared ScriptProperties, which extends the poisoning across every user of the deployment: one anonymous crafted request, and all subsequent visitors run with the attacker-persisted token and host.

I demonstrated the full chain live on a deployment of the unmodified sample (credential POST captured on an external listener; attacker-issued token accepted and persisted; an anonymous third-party visitor shown running on the attacker state). Report that motivated this fix: Google VRP iss.ee/551231684 (closed under the OT2/OT3 tier policy, which is why I'm following up with a fix here as suggested in the reply).

The changes

  • samples/AdobeSign.gs / samples/ZohoCRM.gs: validate the callback parameter against the vendor's own domains (Adobe: adobe.com / adobesign.com / echosign.com; Zoho: accounts.zoho.*) before it reaches setTokenUrl() or storage; anything else returns Denied. with no outbound request.
  • samples/AdobeSign.gs: drop the second setPropertyStore(PropertiesService.getScriptProperties()) call so tokens stay in per-user properties instead of the deployment-wide shared store.

Verification

Re-ran the attack chain against the patched sample: the crafted callback is now rejected (Denied.) with no token request leaving the server, while a legitimate vendor access point continues to flow through unchanged.

A possible follow-up (out of scope for this minimal fix): Service_.handleCallback could validate the state parameter so callbacks can't be replayed with an arbitrary state.

…ples

The Adobe Sign and Zoho CRM samples read a host/URL from an OAuth callback
query parameter (api_access_point / accounts-server) and pass it straight
into setTokenUrl(), then persist it. In a web-app or add-on deployment the
callback is reachable with attacker-chosen parameters, so a single crafted
GET makes the Apps Script server POST the token exchange — authorization
code, client_id and client_secret — to an arbitrary host, and every later
token refresh is sent there as well. In the Adobe Sign sample the second
setPropertyStore call additionally moves the stored state into shared
ScriptProperties, so one visitor's crafted authorization persists the
attacker endpoint for all other users of the deployment.

This change:

- Validates the api_access_point / accounts-server parameter against the
  vendor's own domains (Adobe: adobe.com / adobesign.com / echosign.com;
  Zoho: accounts.zoho.*) and rejects anything else before it reaches
  setTokenUrl() or storage.
- Removes the second setPropertyStore call in the Adobe Sign sample so
  tokens stay in per-user properties.

Verified against the live attack chain from the report that motivated
this fix (Google VRP iss.ee/551231684): after the change the crafted
callback is rejected (returns "Denied.") without any outbound token
request, while a legitimate vendor access point continues to work.
@google-cla

google-cla Bot commented Aug 24, 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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant