From 3370e4586f38f5a805b81c6520c45d5ff997d769 Mon Sep 17 00:00:00 2001 From: Dean Wood Date: Tue, 14 May 2024 15:39:08 -0700 Subject: [PATCH] Updated validation of the web-sdk bridge URL. --- server/meta.jsx | 40 ++++++++-- server/meta.test.js | 176 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 186 insertions(+), 30 deletions(-) diff --git a/server/meta.jsx b/server/meta.jsx index 6821c2ff..0e4ed4e4 100644 --- a/server/meta.jsx +++ b/server/meta.jsx @@ -29,6 +29,7 @@ type SDKMeta = {| |}; const emailRegex = /^.+@.+$/; +const semverRegex = /^[0-9.A-Za-z-+]+$/; function validatePaymentsSDKUrl({ pathname, query, hash }) { if (pathname !== SDK_PATH) { @@ -83,12 +84,40 @@ function validateLegacySDKUrl({ pathname }) { } } -function validateWebSDKUrl({ pathname }) { +function validateWebSDKUrl({ pathname, query }) { if (pathname !== WEB_SDK_BRIDGE_PATH) { throw new Error( `Invalid path for web-sdk bridge url: ${pathname || "undefined"}` ); } + // check for extraneous parameters + Object.keys(query).forEach((param) => { + if (param !== "version" && param !== "origin") { + throw new Error(`Invalid parameter on web-sdk bridge url: ${param}`); + } + }); + // validate the version parameter + if (query.version === undefined || !semverRegex.test(query.version)) { + throw new Error( + `Invalid version parameter on web-sdk bridge url: ${query.version}` + ); + } + // validate the origin parameter + let url = null; + try { + // eslint-disable-next-line compat/compat + url = new URL(query.origin); + } catch (error) { + throw new Error( + `Invalid origin parameter on web-sdk bridge url: ${query.origin}` + ); + } + // check that the origin URL only includes the origin + if (query.origin !== `${url.protocol}//${url.host}`) { + throw new Error( + `Invalid origin parameter on web-sdk bridge url: ${query.origin}` + ); + } } function isLegacySDKUrl(hostname: string, pathname: string): boolean { @@ -186,7 +215,7 @@ function validateSDKUrl(sdkUrl: string) { if (isLegacySDKUrl(hostname, pathname)) { validateLegacySDKUrl({ pathname }); } else if (isWebSDKUrl(hostname, pathname)) { - validateWebSDKUrl({ hostname, pathname }); + validateWebSDKUrl({ pathname, query }); } else if (isSDKUrl(hostname)) { if (hostname !== HOST.LOCALHOST && protocol !== PROTOCOL.HTTPS) { throw new Error( @@ -267,11 +296,8 @@ function sanitizeSDKUrl(sdkUrl: string): string { // eslint-disable-next-line compat/compat const url = new URL(sdkUrl); - // remove query string params for checkout.js and web-sdk - if ( - isLegacySDKUrl(url.hostname, url.pathname) || - isWebSDKUrl(url.hostname, url.pathname) - ) { + // remove query string params for checkout.js + if (isLegacySDKUrl(url.hostname, url.pathname)) { url.search = ""; url.hash = ""; diff --git a/server/meta.test.js b/server/meta.test.js index 2a4b2142..2e32da53 100644 --- a/server/meta.test.js +++ b/server/meta.test.js @@ -1223,49 +1223,179 @@ test("should error when invalid characters are found in the subdomain - we allow }); test("should construct a valid web-sdk bridge url", () => { - const sdkUrl = "https://www.paypal.com/web-sdk/v6/bridge"; + const sdkUrl = + "https://www.paypal.com/web-sdk/v6/bridge?version=1.2.3&origin=https%3A%2F%2Fwww.example.com%3A8000"; + const sdkUID = "abc123"; const { getSDKLoader } = unpackSDKMeta( Buffer.from( JSON.stringify({ url: sdkUrl, + attrs: { + "data-uid": sdkUID, + }, }) ).toString("base64") ); const $ = cheerio.load(getSDKLoader()); - const src = $("script").attr("src"); + const script = $("script"); + const src = script.attr("src"); + const uid = script.attr("data-uid"); if (src !== sdkUrl) { throw new Error(`Expected script url to be ${sdkUrl} - got ${src}`); } + if (uid !== sdkUID) { + throw new Error(`Expected data UID be ${sdkUID} - got ${uid}`); + } }); -test("should prevent query string parameters and hashs on the web-sdk bridge url", () => { +test("should error when extra parameters are present", () => { const sdkUrl = - "https://www.paypal.com/web-sdk/v6/bridge?name=value#hashvalue"; + "https://www.paypal.com/web-sdk/v6/bridge?version=1.2.3&origin=https%3A%2F%2Fwww.example.com%3A8000&name=value"; - const { getSDKLoader } = unpackSDKMeta( - Buffer.from( - JSON.stringify({ - url: sdkUrl, - }) - ).toString("base64") - ); + let error = null; + try { + unpackSDKMeta( + Buffer.from( + JSON.stringify({ + url: sdkUrl, + attrs: { + "data-uid": "abc123", + }, + }) + ).toString("base64") + ); + } catch (err) { + error = err; + } - const $ = cheerio.load(getSDKLoader()); - const script = $("script"); - const src = script.attr("src"); + if (!error) { + throw new Error("Expected error to be thrown"); + } +}); - // eslint-disable-next-line compat/compat - const urlObject = new URL(sdkUrl); - // we expect the query string params to be stripped out - urlObject.search = ""; - // we expect the hash to be stripped out - urlObject.hash = ""; - const expectedUrl = urlObject.toString(); +test("should error when the version parameter is missing", () => { + const sdkUrl = + "https://www.paypal.com/web-sdk/v6/bridge?origin=https%3A%2F%2Fwww.example.com%3A8000"; - if (src !== expectedUrl) { - throw new Error(`Expected script url to be ${expectedUrl} - got ${src}`); + let error = null; + try { + unpackSDKMeta( + Buffer.from( + JSON.stringify({ + url: sdkUrl, + attrs: { + "data-uid": "abc123", + }, + }) + ).toString("base64") + ); + } catch (err) { + error = err; + } + + if (!error) { + throw new Error("Expected error to be thrown"); + } +}); + +test("should error when the version parameter is invalid", () => { + const sdkUrl = + "https://www.paypal.com/web-sdk/v6/bridge?version=^1.2.3&origin=https%3A%2F%2Fwww.example.com%3A8000"; + + let error = null; + try { + unpackSDKMeta( + Buffer.from( + JSON.stringify({ + url: sdkUrl, + attrs: { + "data-uid": "abc123", + }, + }) + ).toString("base64") + ); + } catch (err) { + error = err; + } + + if (!error) { + throw new Error("Expected error to be thrown"); + } +}); + +test("should error when the origin parameter is missing", () => { + const sdkUrl = "https://www.paypal.com/web-sdk/v6/bridge?version=1.2.3"; + + let error = null; + try { + unpackSDKMeta( + Buffer.from( + JSON.stringify({ + url: sdkUrl, + attrs: { + "data-uid": "abc123", + }, + }) + ).toString("base64") + ); + } catch (err) { + error = err; + } + + if (!error) { + throw new Error("Expected error to be thrown"); + } +}); + +test("should error when the origin parameter is invalid", () => { + const sdkUrl = + "https://www.paypal.com/web-sdk/v6/bridge?version=1.2.3&origin=example"; + + let error = null; + try { + unpackSDKMeta( + Buffer.from( + JSON.stringify({ + url: sdkUrl, + attrs: { + "data-uid": "abc123", + }, + }) + ).toString("base64") + ); + } catch (err) { + error = err; + } + + if (!error) { + throw new Error("Expected error to be thrown"); + } +}); + +test("should error when the origin parameter is not just the origin", () => { + const sdkUrl = + "https://www.paypal.com/web-sdk/v6/bridge?version=1.2.3&origin=https%3A%2F%2Fwww.example.com%3A8000%2Fpath"; + + let error = null; + try { + unpackSDKMeta( + Buffer.from( + JSON.stringify({ + url: sdkUrl, + attrs: { + "data-uid": "abc123", + }, + }) + ).toString("base64") + ); + } catch (err) { + error = err; + } + + if (!error) { + throw new Error("Expected error to be thrown"); } });