CVE-2023-31143 (published 2023-05-09T15:15Z)

mage-ai is an open-source data pipeline tool for transforming and integrating data. Those who use Mage starting in version 0.8.34 and prior to 0.8.72 with user authentication enabled may be affected by a vulnerability. The terminal could be accessed by users who are not signed in or do not have editor permissions. Version 0.8.72 contains a fix for this issue.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-306
cwe_name: Missing Authentication for Critical Function
description: The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/mage-ai/mage-ai
commit_details: repo_url: https://github.com/mage-ai/mage-ai
author: David Yang
author_date: 2023-05-04 11:26:49-07:00
msg: [dy] Fix terminal user authentication (#2586)

* [dy] Update terminal user authentication

* [dy] Update auth
repository: repo_url: https://github.com/mage-ai/mage-ai
repo_name: mage-ai/mage-ai
description: 🧙 Build, run, and manage data pipelines for integrating and transforming data.
repo_language: Python
owner: mage-ai
file_changes: filename: index.tsx
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,7 @@
 import Ansi from 'ansi-to-react';
 import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
 
+import AuthToken from '@api/utils/AuthToken';
 import ClickOutside from '@oracle/components/ClickOutside';
 import Text from '@oracle/elements/Text';
 import {
@@ -26,6 +27,7 @@ import {
   KEY_CODE_META,
   KEY_CODE_V,
 } from '@utils/hooks/keyboardShortcuts/constants';
+import { OAUTH2_APPLICATION_CLIENT_ID } from '@api/constants';
 import { onlyKeysPresent } from '@utils/hooks/keyboardShortcuts/utils';
 import { pauseEvent } from '@utils/events';
 import { useKeyboardContext } from '@context/Keyboard';
@@ -58,6 +60,14 @@ function Terminal({
 
   const [stdout, setStdout] = useState<string>();
 
+  const token = useMemo(() => new AuthToken(), []);
+  const oauthWebsocketData = useMemo(() => ({
+    api_key: OAUTH2_APPLICATION_CLIENT_ID,
+    token: token.decodedToken.token,
+  }), [
+    token,
+  ]);
+
   useEffect(() => {
     if (lastMessage) {
       const msg = JSON.parse(lastMessage.data);
@@ -122,12 +132,10 @@ function Terminal({
   }, [command]);
 
   const sendCommand = useCallback((cmd) => {
-    sendMessage(JSON.stringify([
-      'stdin', cmd,
-    ]));
-    sendMessage(JSON.stringify([
-      'stdin', '\r',
-    ]));
+    sendMessage(JSON.stringify({
+      ...oauthWebsocketData,
+      command: ['stdin', cmd + '\r'],
+    }));
     if (cmd?.length >= 2) {
       setCommandIndex(commandHistory.length + 1);
       setCommandHistory(prev => prev.concat(cmd));
@@ -174,12 +182,14 @@ function Terminal({
         pauseEvent(event);
         if (onlyKeysPresent([KEY_CODE_CONTROL, KEY_CODE_C], keyMapping)) {
           if (command?.length >= 0) {
-            sendMessage(JSON.stringify([
-              'stdin', command,
-            ]));
-            sendMessage(JSON.stringify([
-              'stdin', '\x03',
-            ]));
+            sendMessage(JSON.stringify({
+              ...oauthWebsocketData,
+              command: ['stdin', command],
+            }));
+            sendMessage(JSON.stringify({
+              ...oauthWebsocketData,
+              command: ['stdin', '\x03'],
+            }));
             setCursorIndex(0);
           }
           setCommand('');

programming_language: TypeScript
filename: terminal_server.py
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,14 @@
-from mage_ai.api.utils import authenticate_client_and_token
+from mage_ai.api.utils import (
+    authenticate_client_and_token,
+    has_at_least_editor_role,
+)
 from mage_ai.orchestration.db.models.oauth import Oauth2Application
-from mage_ai.settings import REQUIRE_USER_AUTHENTICATION
+from mage_ai.settings import (
+    is_disable_pipeline_edit_access,
+    REQUIRE_USER_AUTHENTICATION,
+)
+from tornado import gen
+import json
 import terminado
 import tornado.websocket
 import re
@@ -78,8 +86,6 @@ class TerminalWebsocketServer(terminado.TermSocket):
                     oauth_token.user
                 if valid:
                     user = oauth_token.user
-                else:
-                    raise Exception('Invalid token')
 
         self.term_name = term_name if term_name else 'tty'
         if user:
@@ -91,6 +97,33 @@ class TerminalWebsocketServer(terminado.TermSocket):
         self.terminal.clients.append(self)
         self.__initiate_terminal(self.terminal)
 
+    @gen.coroutine
+    def on_message(self, raw_message):
+        message = json.loads(raw_message)
+
+        api_key = message.get('api_key')
+        token = message.get('token')
+        command = message.get('command')
+
+        if REQUIRE_USER_AUTHENTICATION or is_disable_pipeline_edit_access():
+            valid = False
+
+            if api_key and token:
+                oauth_client = Oauth2Application.query.filter(
+                    Oauth2Application.client_id == api_key,
+                ).first()
+                if oauth_client:
+                    oauth_token, valid = authenticate_client_and_token(oauth_client.id, token)
+                    valid = valid and \
+                        oauth_token and \
+                        oauth_token.user and \
+                        has_at_least_editor_role(oauth_token.user)
+            if not valid or is_disable_pipeline_edit_access():
+                return self.send_json_message(
+                    ['stdout', f'{command[1]}\nUnauthorized access to the terminal.'])
+
+        super().on_message(json.dumps(command))
+
     def __initiate_terminal(self, terminal):
         self.send_json_message(["setup", {}])
         self._logger.info("TermSocket.open: Opened %s", self.term_name)

programming_language: Python