diff --git a/samples/AdobeSign.gs b/samples/AdobeSign.gs index 688cc65c..13130165 100644 --- a/samples/AdobeSign.gs +++ b/samples/AdobeSign.gs @@ -58,8 +58,10 @@ function getService_(optApiAccessPoint) { .setCallbackFunction('authCallback') // Set the property store where authorized tokens should be persisted. + // Use per-user properties only: script properties are shared by every + // user of a deployment, which would leak one user's tokens and stored + // API access point to all other users. .setPropertyStore(PropertiesService.getUserProperties()) - .setPropertyStore(PropertiesService.getScriptProperties()) // Set the scopes. .setScope('user_read'); @@ -82,6 +84,13 @@ function getService_(optApiAccessPoint) { function authCallback(request) { // Get the API access point specified in the URL parameters. var apiAccessPoint = request.parameter[API_ACCESS_POINT_KEY]; + // Only accept API access points on Adobe-owned hosts. This parameter is + // attacker-controllable in web-app/add-on deployments, and an unchecked + // value sends the token exchange (code, client_id, client_secret) and all + // later refreshes to an arbitrary host. + if (apiAccessPoint && !isValidApiAccessPoint_(apiAccessPoint)) { + return HtmlService.createHtmlOutput('Denied.'); + } var service = getService_(apiAccessPoint); var authorized = service.handleCallback(request); if (authorized) { @@ -93,6 +102,21 @@ function authCallback(request) { } } +/** + * Determines whether the given API access point is on an Adobe-owned host. + * @param {string} apiAccessPoint The API access point URL to validate. + * @return {boolean} True if the host is an Adobe domain served over HTTPS. + */ +function isValidApiAccessPoint_(apiAccessPoint) { + var match = /^https:\/\/([a-zA-Z0-9.-]+)(\/|$)/.exec(apiAccessPoint); + if (!match) { + return false; + } + var host = match[1].toLowerCase(); + return /(^|\.)adobe\.com$/.test(host) || /(^|\.)adobesign\.com$/.test(host) || + /(^|\.)echosign\.com$/.test(host); +} + /** * Logs the redict URI to register in the Dropbox application settings. */ diff --git a/samples/ZohoCRM.gs b/samples/ZohoCRM.gs index 7428cdb3..d1dcb47f 100644 --- a/samples/ZohoCRM.gs +++ b/samples/ZohoCRM.gs @@ -81,6 +81,13 @@ function getService_(optAccountServer) { */ function authCallback(request) { var accountServer = request.parameter['accounts-server']; + // Only accept account servers on Zoho-owned hosts. This parameter is + // attacker-controllable in web-app/add-on deployments, and an unchecked + // value sends the token exchange (code, client_id, client_secret) and all + // later refreshes to an arbitrary host. + if (accountServer && !isValidAccountServer_(accountServer)) { + return HtmlService.createHtmlOutput('Denied.'); + } var service = getService_(accountServer); var authorized = service.handleCallback(request); if (authorized) { @@ -92,6 +99,15 @@ function authCallback(request) { } } +/** + * Determines whether the given account server is a Zoho accounts host. + * @param {string} accountServer The account server URL to validate. + * @return {boolean} True if it is an https accounts.zoho.* host. + */ +function isValidAccountServer_(accountServer) { + return /^https:\/\/accounts\.zoho\.[a-z.]+\/?$/.test(accountServer); +} + /** * Logs the redict URI to register. */