CVE-2024-24566: Other (NVD-CWE-Other)
value: Lobe Chat is a chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. When the application is password-protected (deployed with the `ACCESS_CODE` option), it is possible to access plugins without proper authorization (without password). This vulnerability is patched in 0.122.4.
CWE description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
CVSS v3 base score: 5.3
Published: 2024-01-31T17:15Z
Repository: https://github.com/lobehub/lobe-chat
Language: TypeScript
Files changed: src/app/api/openai/chat/createChatCompletion.ts, src/app/api/openai/createBizOpenAI/index.ts, src/app/api/plugin/gateway/route.ts, src/app/chat/features/ChatHeader/SettingButton.tsx, src/app/chat/settings/(desktop)/index.tsx, src/services/chat.ts
Commit date: 2024-01-30 23:36:02 +0800
Commit message: 🚑️ fix: fix plugin gateway auth (#1195)

* 🚑️ fix: fix plugin auth

* 💚 fix: fix build

* 🐛 fix: fix query of chat settings

* 🐛 fix: fix auth
Diff: diff --git a/src/app/api/openai/errorResponse.test.ts b/src/app/api/errorResponse.test.ts
similarity index 100%
rename from src/app/api/openai/errorResponse.test.ts
rename to src/app/api/errorResponse.test.ts
diff --git a/src/app/api/openai/errorResponse.ts b/src/app/api/errorResponse.ts
similarity index 100%
rename from src/app/api/openai/errorResponse.ts
rename to src/app/api/errorResponse.ts
diff --git a/src/app/api/openai/chat/createChatCompletion.ts b/src/app/api/openai/chat/createChatCompletion.ts
index cb3ae659..7321c27d 100644
--- a/src/app/api/openai/chat/createChatCompletion.ts
+++ b/src/app/api/openai/chat/createChatCompletion.ts
@@ -2,11 +2,11 @@ import { OpenAIStream, StreamingTextResponse } from 'ai';
 import OpenAI from 'openai';
 
 import { ChatErrorType } from '@/types/fetch';
 import { OpenAIChatStreamPayload } from '@/types/openai/chat';
 
-import { createErrorResponse } from '../errorResponse';
+import { createErrorResponse } from '../../errorResponse';
 import { desensitizeUrl } from './desensitizeUrl';
 
 interface CreateChatCompletionOptions {
   openai: OpenAI;
   payload: OpenAIChatStreamPayload;
diff --git a/src/app/api/openai/createBizOpenAI/index.ts b/src/app/api/openai/createBizOpenAI/index.ts
index 40eaa2eb..302d06dc 100644
--- a/src/app/api/openai/createBizOpenAI/index.ts
+++ b/src/app/api/openai/createBizOpenAI/index.ts
@@ -3,11 +3,11 @@ import OpenAI from 'openai';
 import { checkAuth } from '@/app/api/auth';
 import { getServerConfig } from '@/config/server';
 import { getOpenAIAuthFromRequest } from '@/const/fetch';
 import { ChatErrorType, ErrorType } from '@/types/fetch';
 
-import { createErrorResponse } from '../errorResponse';
+import { createErrorResponse } from '../../errorResponse';
 import { createAzureOpenai } from './createAzureOpenai';
 import { createOpenai } from './createOpenai';
 
 /**
  * createOpenAI Instance with Auth and azure openai support
diff --git a/src/app/api/plugin/gateway/route.ts b/src/app/api/plugin/gateway/route.ts
index c2745d75..c8401aa3 100644
--- a/src/app/api/plugin/gateway/route.ts
+++ b/src/app/api/plugin/gateway/route.ts
@@ -1,13 +1,39 @@
 import { createGatewayOnEdgeRuntime } from '@lobehub/chat-plugins-gateway';
 
-import { parserPluginSettings } from '@/app/api/plugin/gateway/settings';
+import { createErrorResponse } from '@/app/api/errorResponse';
 import { getServerConfig } from '@/config/server';
+import { getOpenAIAuthFromRequest } from '@/const/fetch';
+import { ChatErrorType, ErrorType } from '@/types/fetch';
+
+import { parserPluginSettings } from './settings';
+
+const checkAuth = (accessCode: string | null) => {
+  const { ACCESS_CODES } = getServerConfig();
+
+  // if accessCode doesn't exist
+  if (!ACCESS_CODES.length) return { auth: true };
+
+  if (!accessCode || !ACCESS_CODES.includes(accessCode)) {
+    return { auth: false, error: ChatErrorType.InvalidAccessCode };
+  }
+
+  return { auth: true };
+};
 
 const { PLUGINS_INDEX_URL: pluginsIndexUrl, PLUGIN_SETTINGS } = getServerConfig();
 
 const defaultPluginSettings = parserPluginSettings(PLUGIN_SETTINGS);
 
-export const POST = createGatewayOnEdgeRuntime({
-  defaultPluginSettings,
-  pluginsIndexUrl,
-});
+const handler = createGatewayOnEdgeRuntime({ defaultPluginSettings, pluginsIndexUrl });
+
+export const POST = async (req: Request) => {
+  const { accessCode } = getOpenAIAuthFromRequest(req);
+
+  const result = checkAuth(accessCode);
+
+  if (!result.auth) {
+    return createErrorResponse(result.error as ErrorType);
+  }
+
+  return handler(req);
+};
diff --git a/src/app/chat/features/ChatHeader/SettingButton.tsx b/src/app/chat/features/ChatHeader/SettingButton.tsx
index 4a059065..ee2f3981 100644
--- a/src/app/chat/features/ChatHeader/SettingButton.tsx
+++ b/src/app/chat/features/ChatHeader/SettingButton.tsx
@@ -25,11 +25,11 @@ const SettingButton = memo<{ mobile?: boolean }>(({ mobile }) => {
             settingsTab: SettingsTabs.Agent,
             sidebarKey: SidebarTabKey.Setting,
           });
           router.push('/settings/agent');
         } else {
-          router.push(pathString('/chat/settings', { hash: location.hash }));
+          router.push(pathString('/chat/settings', { search: location.search }));
         }
       }}
       size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
       title={t('header.session', { ns: 'setting' })}
     />
diff --git a/src/app/chat/settings/(desktop)/index.tsx b/src/app/chat/settings/(desktop)/index.tsx
index a385f24d..fc0f18a5 100644
--- a/src/app/chat/settings/(desktop)/index.tsx
+++ b/src/app/chat/settings/(desktop)/index.tsx
@@ -1,21 +1,25 @@
 'use client';
 
 import dynamic from 'next/dynamic';
 import { FC, memo } from 'react';
 
+import SessionHydration from '@/app/chat/components/SessionHydration';
 import ResponsiveIndex from '@/components/ResponsiveIndex';
 
 import EditPage from '../features/EditPage';
 import Layout from './layout.desktop';
 
 const Mobile: FC = dynamic(() => import('../(mobile)'), { ssr: false }) as FC;
 
 const ChatSettings = memo(() => (
-  <ResponsiveIndex Mobile={Mobile}>
-    <Layout>
-      <EditPage />
-    </Layout>
-  </ResponsiveIndex>
+  <>
+    <ResponsiveIndex Mobile={Mobile}>
+      <Layout>
+        <EditPage />
+      </Layout>
+    </ResponsiveIndex>
+    <SessionHydration />
+  </>
 ));
 
 export default ChatSettings;
diff --git a/src/services/chat.ts b/src/services/chat.ts
index f56633fa..aac82464 100644
--- a/src/services/chat.ts
+++ b/src/services/chat.ts
@@ -89,11 +89,12 @@ class ChatService {
 
     const gatewayURL = manifest?.gateway;
 
     const res = await fetch(gatewayURL ?? PLUGINS_URLS.gateway, {
       body: JSON.stringify({ ...params, manifest }),
-      headers: createHeadersWithPluginSettings(settings),
+      // TODO: we can have a better auth way
+      headers: createHeadersWithPluginSettings(settings, createHeaderWithOpenAI()),
       method: 'POST',
       signal: options?.signal,
     });
 
     if (!res.ok) {
diff --git a/src/store/global/slices/settings/__snapshots__/selectors.test.ts.snap b/src/store/global/slices/settings/__snapshots__/selectors.test.ts.snap
deleted file mode 100644
index 3a7f56cc..00000000
--- a/src/store/global/slices/settings/__snapshots__/selectors.test.ts.snap
+++ /dev/null
@@ -1,246 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`settingsSelectors > CUSTOM_MODELS > custom deletion, addition, and renaming of models 1`] = `
-[
-  {
-    "displayName": "llama",
-    "name": "llama",
-  },
-  {
-    "displayName": "claude-2",
-    "name": "claude-2",
-  },
-  {
-    "displayName": "gpt-4-32k",
-    "name": "gpt-4-1106-preview",
-  },
-]
-`;
-
-exports[`settingsSelectors > CUSTOM_MODELS > duplicate naming model 1`] = `
-[
-  {
-    "displayName": "gpt-3.5-turbo",
-    "name": "gpt-3.5-turbo",
-  },
-  {
-    "displayName": "gpt-3.5-turbo-1106",
-    "name": "gpt-3.5-turbo-1106",
-  },
-  {
-    "displayName": "gpt-3.5-turbo-16k",
-    "name": "gpt-3.5-turbo-16k",
-  },
-  {
-    "displayName": "gpt-4",
-    "name": "gpt-4",
-  },
-  {
-    "displayName": "gpt-4-32k",
-    "name": "gpt-4-32k",
-  },
-  {
-    "displayName": "gpt-4-vision-preview",
-    "name": "gpt-4-vision-preview",
-  },
-  {
-    "displayName": "gpt-4-32k",
-    "name": "gpt-4-1106-preview",
-  },
-]
-`;
-
-exports[`settingsSelectors > CUSTOM_MODELS > only add the model 1`] = `
-[
-  {
-    "displayName": "gpt-3.5-turbo",
-    "name": "gpt-3.5-turbo",
-  },
-  {
-    "displayName": "gpt-3.5-turbo-1106",
-    "name": "gpt-3.5-turbo-1106",
-  },
-  {
-    "displayName": "gpt-3.5-turbo-16k",
-    "name": "gpt-3.5-turbo-16k",
-  },
-  {
-    "displayName": "gpt-4",
-    "name": "gpt-4",
-  },
-  {
-    "displayName": "gpt-4-32k",
-    "name": "gpt-4-32k",
-  },
-  {
-    "displayName": "gpt-4-1106-preview",
-    "name": "gpt-4-1106-preview",
-  },
-  {
-    "displayName": "gpt-4-vision-preview",
-    "name": "gpt-4-vision-preview",
-  },
-  {
-    "displayName": "model1",
-    "name": "model1",
-  },
-  {
-    "displayName": "model2",
-    "name": "model2",
-  },
-  {
-    "displayName": "model3",
-    "name": "model3",
-  },
-  {
-    "displayName": "model4",
-    "name": "model4",
-  },
-]
-`;
-
-exports[`settingsSelectors > currentSettings > should merge DEFAULT_SETTINGS and s.settings correctly 1`] = `
-{
-  "avatar": "avatar.jpg",
-  "defaultAgent": {
-    "config": {
-      "autoCreateTopicThreshold": 2,
-      "displayMode": "chat",
-      "enableAutoCreateTopic": true,
-      "historyCount": 1,
-      "model": "gpt-3.5-turbo",
-      "params": {
-        "frequency_penalty": 0,
-        "presence_penalty": 0,
-        "temperature": 0.6,
-        "top_p": 1,
-      },
-      "plugins": [],
-      "systemRole": "",
-      "tts": {
-        "showAllLocaleVoice": false,
-        "sttLocale": "auto",
-        "ttsService": "openai",
-        "voice": {
-          "openai": "alloy",
-        },
-      },
-    },
-    "meta": {
-      "avatar": "Default Agent",
-      "description": "Default agent for testing",
-    },
-  },
-  "fontSize": 14,
-  "language": "en-US",
-  "languageModel": {
-    "openAI": {
-      "OPENAI_API_KEY": "openai-api-key",
-      "endpoint": "https://openai-endpoint.com",
-      "models": [
-        "gpt-3.5-turbo",
-      ],
-    },
-  },
-  "neutralColor": "sand",
-  "password": "password123",
-  "primaryColor": "blue",
-  "themeMode": "light",
-  "tool": {
-    "dalle": {
-      "autoGenerate": false,
-    },
-  },
-  "tts": {
-    "openAI": {
-      "sttModel": "whisper-1",
-      "ttsModel": "tts-1",
-    },
-    "sttAutoStop": true,
-    "sttServer": "openai",
-  },
-}
-`;
-
-exports[`settingsSelectors > currentTTS > should merge DEFAULT_TTS_CONFIG and s.settings.tts correctly 1`] = `
-{
-  "openAI": {
-    "sttModel": "whisper-2",
-    "ttsModel": "tts-1",
-  },
-  "sttAutoStop": false,
-  "sttServer": "openai",
-}
-`;
-
-exports[`settingsSelectors > dalleConfig > should return the dalle configuration 1`] = `
-{
-  "apiKey": "dalle-api-key",
-  "autoGenerate": true,
-}
-`;
-
-exports[`settingsSelectors > defaultAgent > should merge DEFAULT_AGENT and s.settings.defaultAgent correctly 1`] = `
-{
-  "config": {
-    "autoCreateTopicThreshold": 2,
-    "displayMode": "chat",
-    "enableAutoCreateTopic": true,
-    "historyCount": 1,
-    "model": "gpt-3.5-turbo",
-    "params": {
-      "frequency_penalty": 0,
-      "presence_penalty": 0,
-      "temperature": 0.6,
-      "top_p": 1,
-    },
-    "plugins": [],
-    "systemRole": "user",
-    "tts": {
-      "showAllLocaleVoice": false,
-      "sttLocale": "auto",
-      "ttsService": "openai",
-      "voice": {
-        "openai": "alloy",
-      },
-    },
-  },
-  "meta": {
-    "avatar": "agent-avatar.jpg",
-    "description": "Test agent",
-  },
-}
-`;
-
-exports[`settingsSelectors > defaultAgentConfig > should merge DEFAULT_AGENT_CONFIG and defaultAgent(s).config correctly 1`] = `
-{
-  "autoCreateTopicThreshold": 2,
-  "displayMode": "chat",
-  "enableAutoCreateTopic": true,
-  "historyCount": 1,
-  "model": "gpt-3.5-turbo",
-  "params": {
-    "frequency_penalty": 0,
-    "presence_penalty": 0,
-    "temperature": 0.7,
-    "top_p": 1,
-  },
-  "plugins": [],
-  "systemRole": "user",
-  "tts": {
-    "showAllLocaleVoice": false,
-    "sttLocale": "auto",
-    "ttsService": "openai",
-    "voice": {
-      "openai": "alloy",
-    },
-  },
-}
-`;
-
-exports[`settingsSelectors > defaultAgentMeta > should merge DEFAULT_AGENT_META and defaultAgent(s).meta correctly 1`] = `
-{
-  "avatar": "agent-avatar.jpg",
-  "description": "Test agent",
-}
-`;

Vulnerable code: import { createErrorResponse } from '../errorResponse';
import { createErrorResponse } from '../errorResponse';
import { parserPluginSettings } from '@/app/api/plugin/gateway/settings';
export const POST = createGatewayOnEdgeRuntime({
  defaultPluginSettings,
  pluginsIndexUrl,
});
          router.push(pathString('/chat/settings', { hash: location.hash }));
  <ResponsiveIndex Mobile={Mobile}>
    <Layout>
      <EditPage />
    </Layout>
  </ResponsiveIndex>
      headers: createHeadersWithPluginSettings(settings),
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`settingsSelectors > CUSTOM_MODELS > custom deletion, addition, and renaming of models 1`] = `
[
  {
    "displayName": "llama",
    "name": "llama",
  },
  {
    "displayName": "claude-2",
    "name": "claude-2",
  },
  {
    "displayName": "gpt-4-32k",
    "name": "gpt-4-1106-preview",
  },
]
`;

exports[`settingsSelectors > CUSTOM_MODELS > duplicate naming model 1`] = `
[
  {
    "displayName": "gpt-3.5-turbo",
    "name": "gpt-3.5-turbo",
  },
  {
    "displayName": "gpt-3.5-turbo-1106",
    "name": "gpt-3.5-turbo-1106",
  },
  {
    "displayName": "gpt-3.5-turbo-16k",
    "name": "gpt-3.5-turbo-16k",
  },
  {
    "displayName": "gpt-4",
    "name": "gpt-4",
  },
  {
    "displayName": "gpt-4-32k",
    "name": "gpt-4-32k",
  },
  {
    "displayName": "gpt-4-vision-preview",
    "name": "gpt-4-vision-preview",
  },
  {
    "displayName": "gpt-4-32k",
    "name": "gpt-4-1106-preview",
  },
]
`;

exports[`settingsSelectors > CUSTOM_MODELS > only add the model 1`] = `
[
  {
    "displayName": "gpt-3.5-turbo",
    "name": "gpt-3.5-turbo",
  },
  {
    "displayName": "gpt-3.5-turbo-1106",
    "name": "gpt-3.5-turbo-1106",
  },
  {
    "displayName": "gpt-3.5-turbo-16k",
    "name": "gpt-3.5-turbo-16k",
  },
  {
    "displayName": "gpt-4",
    "name": "gpt-4",
  },
  {
    "displayName": "gpt-4-32k",
    "name": "gpt-4-32k",
  },
  {
    "displayName": "gpt-4-1106-preview",
    "name": "gpt-4-1106-preview",
  },
  {
    "displayName": "gpt-4-vision-preview",
    "name": "gpt-4-vision-preview",
  },
  {
    "displayName": "model1",
    "name": "model1",
  },
  {
    "displayName": "model2",
    "name": "model2",
  },
  {
    "displayName": "model3",
    "name": "model3",
  },
  {
    "displayName": "model4",
    "name": "model4",
  },
]
`;

exports[`settingsSelectors > currentSettings > should merge DEFAULT_SETTINGS and s.settings correctly 1`] = `
{
  "avatar": "avatar.jpg",
  "defaultAgent": {
    "config": {
      "autoCreateTopicThreshold": 2,
      "displayMode": "chat",
      "enableAutoCreateTopic": true,
      "historyCount": 1,
      "model": "gpt-3.5-turbo",
      "params": {
        "frequency_penalty": 0,
        "presence_penalty": 0,
        "temperature": 0.6,
        "top_p": 1,
      },
      "plugins": [],
      "systemRole": "",
      "tts": {
        "showAllLocaleVoice": false,
        "sttLocale": "auto",
        "ttsService": "openai",
        "voice": {
          "openai": "alloy",
        },
      },
    },
    "meta": {
      "avatar": "Default Agent",
      "description": "Default agent for testing",
    },
  },
  "fontSize": 14,
  "language": "en-US",
  "languageModel": {
    "openAI": {
      "OPENAI_API_KEY": "openai-api-key",
      "endpoint": "https://openai-endpoint.com",
      "models": [
        "gpt-3.5-turbo",
      ],
    },
  },
  "neutralColor": "sand",
  "password": "password123",
  "primaryColor": "blue",
  "themeMode": "light",
  "tool": {
    "dalle": {
      "autoGenerate": false,
    },
  },
  "tts": {
    "openAI": {
      "sttModel": "whisper-1",
      "ttsModel": "tts-1",
    },
    "sttAutoStop": true,
    "sttServer": "openai",
  },
}
`;

exports[`settingsSelectors > currentTTS > should merge DEFAULT_TTS_CONFIG and s.settings.tts correctly 1`] = `
{
  "openAI": {
    "sttModel": "whisper-2",
    "ttsModel": "tts-1",
  },
  "sttAutoStop": false,
  "sttServer": "openai",
}
`;

exports[`settingsSelectors > dalleConfig > should return the dalle configuration 1`] = `
{
  "apiKey": "dalle-api-key",
  "autoGenerate": true,
}
`;

exports[`settingsSelectors > defaultAgent > should merge DEFAULT_AGENT and s.settings.defaultAgent correctly 1`] = `
{
  "config": {
    "autoCreateTopicThreshold": 2,
    "displayMode": "chat",
    "enableAutoCreateTopic": true,
    "historyCount": 1,
    "model": "gpt-3.5-turbo",
    "params": {
      "frequency_penalty": 0,
      "presence_penalty": 0,
      "temperature": 0.6,
      "top_p": 1,
    },
    "plugins": [],
    "systemRole": "user",
    "tts": {
      "showAllLocaleVoice": false,
      "sttLocale": "auto",
      "ttsService": "openai",
      "voice": {
        "openai": "alloy",
      },
    },
  },
  "meta": {
    "avatar": "agent-avatar.jpg",
    "description": "Test agent",
  },
}
`;

exports[`settingsSelectors > defaultAgentConfig > should merge DEFAULT_AGENT_CONFIG and defaultAgent(s).config correctly 1`] = `
{
  "autoCreateTopicThreshold": 2,
  "displayMode": "chat",
  "enableAutoCreateTopic": true,
  "historyCount": 1,
  "model": "gpt-3.5-turbo",
  "params": {
    "frequency_penalty": 0,
    "presence_penalty": 0,
    "temperature": 0.7,
    "top_p": 1,
  },
  "plugins": [],
  "systemRole": "user",
  "tts": {
    "showAllLocaleVoice": false,
    "sttLocale": "auto",
    "ttsService": "openai",
    "voice": {
      "openai": "alloy",
    },
  },
}
`;

exports[`settingsSelectors > defaultAgentMeta > should merge DEFAULT_AGENT_META and defaultAgent(s).meta correctly 1`] = `
{
  "avatar": "agent-avatar.jpg",
  "description": "Test agent",
}
`;
Fixed code: import { createErrorResponse } from '../../errorResponse';
import { createErrorResponse } from '../../errorResponse';
import { createErrorResponse } from '@/app/api/errorResponse';
import { getOpenAIAuthFromRequest } from '@/const/fetch';
import { ChatErrorType, ErrorType } from '@/types/fetch';

import { parserPluginSettings } from './settings';

const checkAuth = (accessCode: string | null) => {
  const { ACCESS_CODES } = getServerConfig();

  // if accessCode doesn't exist
  if (!ACCESS_CODES.length) return { auth: true };

  if (!accessCode || !ACCESS_CODES.includes(accessCode)) {
    return { auth: false, error: ChatErrorType.InvalidAccessCode };
  }

  return { auth: true };
};
const handler = createGatewayOnEdgeRuntime({ defaultPluginSettings, pluginsIndexUrl });

export const POST = async (req: Request) => {
  const { accessCode } = getOpenAIAuthFromRequest(req);

  const result = checkAuth(accessCode);

  if (!result.auth) {
    return createErrorResponse(result.error as ErrorType);
  }

  return handler(req);
};
          router.push(pathString('/chat/settings', { search: location.search }));
import SessionHydration from '@/app/chat/components/SessionHydration';
  <>
    <ResponsiveIndex Mobile={Mobile}>
      <Layout>
        <EditPage />
      </Layout>
    </ResponsiveIndex>
    <SessionHydration />
  </>
      // TODO: we can have a better auth way
      headers: createHeadersWithPluginSettings(settings, createHeaderWithOpenAI()),