Yuki Takei 10 месяцев назад
Родитель
Сommit
e0f0684091

+ 12 - 2
apps/app/src/server/routes/apiv3/slack-integration.js

@@ -295,10 +295,20 @@ module.exports = (crowi) => {
   // TODO: this method will be a middleware when typescriptize in the future
   function getResponseUrl(req) {
     const { body } = req;
-    const responseUrl = body?.growiCommand?.responseUrl;
+    const { crowi } = req;
+    const responseUrl = body?.growiCommand?.responseUrl ?? body.response_url;
+
     if (responseUrl == null) {
-      return body.response_url; // may be null
+      return null;
     }
+
+    const proxyUri = crowi.slackIntegrationService.proxyUriForCurrentType;
+    const { isValidResponseUrl } = require('@growi/slack/src/utils/response-url-validator');
+
+    if (!isValidResponseUrl(responseUrl, proxyUri)) {
+      throw createError(400, 'Invalid response_url');
+    }
+
     return responseUrl;
   }
 

+ 12 - 5
packages/slack/src/utils/respond-util-factory.ts

@@ -3,6 +3,7 @@ import urljoin from 'url-join';
 
 import type { IRespondUtil } from '../interfaces/respond-util';
 import type { RespondBodyForResponseUrl } from '../interfaces/response-url';
+import { isValidResponseUrl } from './response-url-validator';
 
 type AxiosOptions = {
   headers?: {
@@ -14,10 +15,16 @@ function getResponseUrlForProxy(proxyUri: string, responseUrl: string): string {
   return urljoin(proxyUri, `/g2s/respond?response_url=${responseUrl}`);
 }
 
-function getUrl(responseUrl: string, proxyUri: string | null): string {
-  return proxyUri == null
+function getUrl(responseUrl: string, proxyUri?: string): string {
+  const finalUrl = proxyUri === undefined
     ? responseUrl
     : getResponseUrlForProxy(proxyUri, responseUrl);
+
+  if (!isValidResponseUrl(responseUrl, proxyUri)) {
+    throw new Error('Invalid final response URL');
+  }
+
+  return finalUrl;
 }
 
 export class RespondUtil implements IRespondUtil {
@@ -27,8 +34,8 @@ export class RespondUtil implements IRespondUtil {
 
   constructor(
     responseUrl: string,
-    proxyUri: string | null,
     appSiteUrl: string,
+    proxyUri?: string,
   ) {
     this.url = getUrl(responseUrl, proxyUri);
 
@@ -89,8 +96,8 @@ export class RespondUtil implements IRespondUtil {
 
 export function generateRespondUtil(
   responseUrl: string,
-  proxyUri: string | null,
   appSiteUrl: string,
+  proxyUri?: string,
 ): RespondUtil {
-  return new RespondUtil(responseUrl, proxyUri, appSiteUrl);
+  return new RespondUtil(responseUrl, appSiteUrl, proxyUri);
 }

+ 36 - 0
packages/slack/src/utils/response-url-validator.ts

@@ -0,0 +1,36 @@
+import { URL } from 'node:url';
+
+const ALLOWED_SLACK_HOST = 'hooks.slack.com';
+
+export function isValidResponseUrl(responseUrl: string, slackbotProxyUri?: string): boolean {
+  try {
+    const parsedUrl = new URL(responseUrl);
+
+    // Case 1: Direct to Slack
+    if (parsedUrl.protocol === 'https:' && parsedUrl.hostname === ALLOWED_SLACK_HOST) {
+      return true;
+    }
+
+    // Case 2: Via slackbot-proxy
+    if (slackbotProxyUri) {
+      const parsedProxyUri = new URL(slackbotProxyUri);
+
+      if (
+        (parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:') &&
+        parsedUrl.hostname === parsedProxyUri.hostname &&
+        parsedUrl.pathname === '/g2s/respond'
+      ) {
+        const slackResponseUrlParam = parsedUrl.searchParams.get('response_url');
+        if (slackResponseUrlParam) {
+          // Recursively validate the response_url parameter
+          return isValidResponseUrl(slackResponseUrlParam); // No proxy URI for the inner check
+        }
+      }
+    }
+
+    return false;
+  } catch (error) {
+    // Invalid URL format
+    return false;
+  }
+}