瀏覽代碼

@uppy/companion: pass fetched origins to window.postMessage() (#5529)

Merlijn Vos 4 月之前
父節點
當前提交
07956e25e2

+ 1 - 0
packages/@uppy/companion/src/companion.js

@@ -165,6 +165,7 @@ module.exports.app = (optionsArg = {}) => {
           key,
           secret,
           redirect_uri: getRedirectUri(),
+          origins: ['http://localhost:5173'],
         },
       })
     })

+ 4 - 4
packages/@uppy/companion/src/server/controllers/connect.js

@@ -3,8 +3,8 @@ const oAuthState = require('../helpers/oauth-state')
 /**
  * Derived from `cors` npm package.
  * @see https://github.com/expressjs/cors/blob/791983ebc0407115bc8ae8e64830d440da995938/lib/index.js#L19-L34
- * @param {string} origin 
- * @param {*} allowedOrigins 
+ * @param {string} origin
+ * @param {*} allowedOrigins
  * @returns {boolean}
  */
 function isOriginAllowed(origin, allowedOrigins) {
@@ -17,7 +17,6 @@ function isOriginAllowed(origin, allowedOrigins) {
   return allowedOrigins.test?.(origin) ?? !!allowedOrigins;
 }
 
-
 const queryString = (params, prefix = '?') => {
   const str = new URLSearchParams(params).toString()
   return str ? `${prefix}${str}` : ''
@@ -66,7 +65,7 @@ function getClientOrigin(base64EncodedState) {
  *
  * The client has open a new tab and is about to be redirected to the auth
  * provider. When the user will return to companion, we'll have to send the auth
- * token back to Uppy with `window.postMessage()`. 
+ * token back to Uppy with `window.postMessage()`.
  * To prevent other tabs and unauthorized origins from accessing that token, we
  * reuse origin(s) from `corsOrigins` to limit the scope of `postMessage()`, which
  * has `targetOrigin` parameter, required for cross-origin messages (i.e. if Uppy
@@ -113,3 +112,4 @@ module.exports = function connect(req, res, next) {
   }
   encodeStateAndRedirect(req, res, stateObj)
 }
+module.exports.isOriginAllowed = isOriginAllowed

+ 23 - 10
packages/@uppy/companion/src/server/controllers/send-token.js

@@ -1,4 +1,5 @@
 const serialize = require('serialize-javascript')
+const { isOriginAllowed } = require('./connect')
 
 const oAuthState = require('../helpers/oauth-state')
 
@@ -46,18 +47,30 @@ const htmlContent = (token, origin) => {
 
 /**
  *
- * @param {object} req
- * @param {object} res
- * @param {Function} next
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ * @param {import('express').NextFunction} next
  */
-module.exports = function sendToken (req, res, next) {
-  const uppyAuthToken = req.companion.authToken
+module.exports = function sendToken(req, res, next) {
+  // @ts-expect-error untyped
+  const { companion } = req
+  const uppyAuthToken = companion.authToken
 
   const { state } = oAuthState.getGrantDynamicFromRequest(req)
-  if (state) {
-    const origin = oAuthState.getFromState(state, 'origin', req.companion.options.secret)
-    res.send(htmlContent(uppyAuthToken, origin))
-    return
+
+  if (!state) {
+    return next()
   }
-  next()
+
+  const clientOrigin = oAuthState.getFromState(state, 'origin', companion.options.secret)
+  const customerDefinedAllowedOrigins = oAuthState.getFromState(state, 'customerDefinedAllowedOrigins', companion.options.secret)
+
+  if (
+    customerDefinedAllowedOrigins &&
+    !isOriginAllowed(clientOrigin, customerDefinedAllowedOrigins)
+  ) {
+    return next()
+  }
+
+  return res.send(htmlContent(uppyAuthToken, clientOrigin))
 }

+ 22 - 0
packages/@uppy/companion/src/server/provider/credentials.js

@@ -108,10 +108,32 @@ exports.getCredentialsOverrideMiddleware = (providers, companionOptions) => {
 
       const credentials = await fetchProviderKeys(providerName, companionOptions, payload)
 
+      // Besides the key and secret the fetched credentials can also contain `origins`,
+      // which is an array of strings of allowed origins to prevent any origin from getting the OAuth
+      // token through window.postMessage (see comment in connect.js).
+      // postMessage happens in send-token.js, which is a different request, so we need to put the allowed origins
+      // on the encrypted session state to access it later there.
+      if (Array.isArray(credentials.origins) && credentials.origins.length > 0) {
+        const decodedState = oAuthState.decodeState(state, companionOptions.secret)
+        decodedState.customerDefinedAllowedOrigins = credentials.origins
+        const newState = oAuthState.encodeState(decodedState, companionOptions.secret)
+        // @ts-expect-error untyped
+        req.session.grant = {
+          // @ts-expect-error untyped
+          ...req.session.grant,
+          dynamic: {
+            // @ts-expect-error untyped
+            ...req.session.grant?.dynamic,
+            state: newState,
+          },
+        }
+      }
+
       res.locals.grant = {
         dynamic: {
           key: credentials.key,
           secret: credentials.secret,
+          origins: credentials.origins,
         },
       }
 

+ 1 - 1
packages/@uppy/companion/src/server/provider/index.js

@@ -137,7 +137,7 @@ module.exports.addProviderOptions = (companionOptions, grantConfig, getOauthProv
       grantConfig[oauthProvider].secret = providerOptions[providerName].secret
       if (providerOptions[providerName].credentialsURL) {
         // eslint-disable-next-line no-param-reassign
-        grantConfig[oauthProvider].dynamic = ['key', 'secret', 'redirect_uri']
+        grantConfig[oauthProvider].dynamic = ['key', 'secret', 'redirect_uri', 'origins']
       }
 
       const provider = exports.getDefaultProviders()[providerName]