CVE-2023-40168 (published 2023-08-17T20:15Z)

TurboWarp is a desktop application that compiles scratch projects to JavaScript. TurboWarp Desktop versions prior to version 1.8.0 allowed a malicious project or custom extension to read arbitrary files from disk and upload them to a remote server. The only required user interaction is opening the sb3 file or loading the extension. The web version of TurboWarp is not affected. This bug has been addressed in commit `55e07e99b59` after an initial fix which was reverted. Users are advised to upgrade to version 1.8.0 or later. Users unable to upgrade should avoid opening sb3 files or loading extensions from untrusted sources.

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

Weakness information:
cwe_id: CWE-863
cwe_name: Incorrect Authorization
description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are incorrectly applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

Fix information:
repo_url: https://github.com/TurboWarp/desktop
commit_details: repo_url: https://github.com/TurboWarp/desktop
author: GarboMuffin
author_date: 2023-06-11 15:44:40-05:00
msg: Rewrite CORS enforcement and offline extension cache

This fixes the offline extension cache.
Also added an option to force CORS to be enabled.
repository: repo_url: https://github.com/TurboWarp/desktop
repo_name: TurboWarp/desktop
description: A better offline editor for Scratch 3.
repo_language: JavaScript
owner: TurboWarp
file_changes: filename: en.json
change_type: ModificationType.MODIFY
diff: @@ -15,6 +15,8 @@
   "settings.no-devices-detected": "No devices detected",
   "settings.background-throttling": "Sleep hidden windows",
   "settings.background-throttling-disabled": "Allowing hidden windows to remain active will result in higher CPU usage and lower battery life.",
+  "settings.bypass-cors": "Allow extensions to bypass CORS",
+  "settings.bypass-cors-enabled": "Disabling CORS could allow malicious projects or extensions to access any website or device on your network.",
   "menu.new-window": "New Window",
   "menu.learn-more": "Learn more",
   "unload.stay": "Stay",

programming_language: JavaScript
filename: bypass-cors.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,18 @@
+import {ipcMain} from 'electron';
+import {get, set} from './store';
+
+const BYPASS_CORS_KEY = 'bypass_cors';
+
+const canBypassCORS = () => !!get(BYPASS_CORS_KEY);
+
+ipcMain.on('bypass-cors/get-is-enabled', (e) => {
+  e.returnValue = canBypassCORS();
+});
+
+ipcMain.handle('bypass-cors/set-is-enabled', (e, enabled) => {
+  set(BYPASS_CORS_KEY, enabled);
+});
+
+export {
+  canBypassCORS
+};

programming_language: TypeScript
filename: extensions.js
change_type: ModificationType.MODIFY
diff: @@ -1,17 +1,59 @@
 import {app, protocol} from 'electron';
 import pathUtil from 'path';
 import {staticDir} from './environment';
+import {canBypassCORS} from './bypass-cors';
 
 const extensionDirectory = pathUtil.join(staticDir, 'extensions.turbowarp.org', '/');
 
 app.on('session-created', (session) => {
+  const rootFileURL = new URL(`file://${__dirname}/`).href;
+
+  // We can only use one onBeforeRequest -- calling it again removes the previous listener.
   session.webRequest.onBeforeRequest({
-    urls: ['https://extensions.turbowarp.org/*']
+    urls: ['file://*', 'https://extensions.turbowarp.org/*']
   }, (details, callback) => {
-    const path = new URL(details.url).pathname;
-    callback({
-      redirectURL: `tw-extensions://${path}`
-    });
+    const url = new URL(details.url);
+
+    if (url.protocol === 'file:') {
+      // Prevent file:// URLs from fetching other file:// URLs from outside the app.
+      callback({
+        cancel: !url.href.startsWith(rootFileURL)
+      });
+    } else if (url.origin === 'https://extensions.turbowarp.org') {
+      // Rewrite extensions.turbowarp.org to the offline cache.
+      callback({
+        redirectURL: `tw-extensions://${url.pathname}`
+      });
+    } else {
+      // This should never happen.
+      callback({});
+    }
+  });
+
+  // By default in Electron, file:// URLs bypass CORS. We enforce it ourselves here.
+  session.webRequest.onHeadersReceived((details, callback) => {
+    if (details.resourceType === 'xhr') {
+      const sourceURL = new URL(details.frame.url);
+      const destinationURL = new URL(details.url);
+      if ((destinationURL.protocol === 'http:' || destinationURL.protocol === 'https:') && sourceURL.protocol === 'file:') {
+        if (canBypassCORS()) {
+          callback({
+            responseHeaders: {
+              ...(details.responseHeaders || {}),
+              'access-control-allow-origin': '*'
+            }
+          });
+        } else {
+          const corsHeaders = details.responseHeaders?.['access-control-allow-origin'] || [];
+          const corsHeader = corsHeaders.join(',');
+          callback({
+            cancel: corsHeader !== '*'
+          });
+        }
+        return;
+      }
+    }
+    callback({});
   });
 });
 

programming_language: TypeScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -652,37 +652,6 @@ app.on('session-created', (session) => {
       });
     }
   });
-
-  const rootFileURL = new URL(`file://${__dirname}/`).href;
-
-  // Enforce additional restrictions when fetching file:// URIs
-  session.webRequest.onBeforeRequest((details, callback) => {
-    const destinationURL = new URL(details.url);
-    if (destinationURL.protocol === 'file:' && !destinationURL.href.startsWith(rootFileURL)) {
-      return callback({
-        cancel: true
-      });
-    }
-    callback({});
-  });
-
-  // Enforce CORS when a file:// URI fetches something from the broader internet
-  session.webRequest.onHeadersReceived((details, callback) => {
-    if (details.resourceType === 'xhr') {
-      const sourceURL = new URL(details.frame.url);
-      const destinationURL = new URL(details.url);
-      if ((destinationURL.protocol === 'http:' || destinationURL.protocol === 'https:') && sourceURL.protocol === 'file:') {
-        const corsHeaders = details.responseHeaders?.['access-control-allow-origin'] || [];
-        const corsHeader = corsHeaders.join(',');
-        if (corsHeader !== '*') {
-          return callback({
-            cancel: true
-          });
-        }
-      }
-    }
-    callback({});
-  });
 });
 
 app.on('web-contents-created', (event, webContents) => {

programming_language: TypeScript
filename: desktop-settings.jsx
change_type: ModificationType.MODIFY
diff: @@ -55,8 +55,8 @@ class DesktopSettings extends React.Component {
       mediaDevicesNeedRestart: false,
 
       isHardwareAccelerationEnabled: ipcRenderer.sendSync('hardware-acceleration/get-is-enabled'),
-
-      isBackgroundThrottlingEnabled: ipcRenderer.sendSync('background-throttling/get-is-enabled')
+      isBackgroundThrottlingEnabled: ipcRenderer.sendSync('background-throttling/get-is-enabled'),
+      canBypassCORS: ipcRenderer.sendSync('bypass-cors/get-is-enabled')
     };
 
     this.handleChangeUpdateCheckerEnabled = this.handleChangeUpdateCheckerEnabled.bind(this);
@@ -65,6 +65,7 @@ class DesktopSettings extends React.Component {
     this.handleSelectedVideoDeviceChanged = this.handleSelectedVideoDeviceChanged.bind(this);
     this.handleChangeHardwareAccelerationEnabled = this.handleChangeHardwareAccelerationEnabled.bind(this);
     this.handleBackgroundThrottlingChanged = this.handleBackgroundThrottlingChanged.bind(this);
+    this.handleBypassCORSChanged = this.handleBypassCORSChanged.bind(this);
     this.handleOpenUserData = this.handleOpenUserData.bind(this);
   }
 
@@ -134,6 +135,14 @@ class DesktopSettings extends React.Component {
     });
   }
 
+  handleBypassCORSChanged (e) {
+    const enabled = e.target.checked;
+    ipcRenderer.invoke('bypass-cors/set-is-enabled', enabled);
+    this.setState({
+      canBypassCORS: enabled
+    });
+  }
+
   handleOpenUserData () {
     ipcRenderer.send('open-user-data');
   }
@@ -231,6 +240,22 @@ class DesktopSettings extends React.Component {
           )}
         </div>
 
+        <div className={styles.option}>
+          <label className={styles.label}>
+            <input
+              type="checkbox"
+              checked={this.state.canBypassCORS}
+              onChange={this.handleBypassCORSChanged}
+            />
+            {getTranslation('settings.bypass-cors')}
+          </label>
+          {this.state.canBypassCORS && (
+            <div className={styles.warning}>
+              {getTranslation('settings.bypass-cors-enabled')}
+            </div>
+          )}
+        </div>
+
         <div className={styles.option}>
           <button onClick={this.handleOpenUserData}>
             {getTranslation('settings.open-user-data')}

programming_language: JavaScript
repo_url: https://github.com/TurboWarp/desktop
commit_details: repo_url: https://github.com/TurboWarp/desktop
author: GarboMuffin
author_date: 2023-01-21 01:14:50-06:00
msg: Fix some bugs related to protocols and file: URIs
repository: repo_url: https://github.com/TurboWarp/desktop
repo_name: TurboWarp/desktop
description: A better offline editor for Scratch 3.
repo_language: JavaScript
owner: TurboWarp
file_changes: filename: extensions.js
change_type: ModificationType.MODIFY
diff: @@ -15,15 +15,6 @@ app.on('session-created', (session) => {
   });
 });
 
-protocol.registerSchemesAsPrivileged([
-  {
-    scheme: 'tw-extensions',
-    privileges: {
-      supportFetchAPI: true
-    }
-  }
-]);
-
 app.whenReady().then(() => {
   protocol.registerFileProtocol('tw-extensions', (request, callback) => {
     const pathAndQuery = request.url.substring('tw-extensions://'.length);

programming_language: TypeScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -21,6 +21,8 @@ import './detect-arm-translation';
 import {isBackgroundThrottlingEnabled, whenBackgroundThrottlingChanged} from './background-throttling';
 import './extensions';
 import {createAtomicWriteStream} from './atomic-file-write-stream';
+import './protocols';
+import './request-security';
 
 const readFile = util.promisify(fs.readFile);
 const brotliDecompress = util.promisify(zlib.brotliDecompress);
@@ -883,7 +885,7 @@ if (acquiredLock) {
     }
   });
 
-  app.on('ready', () => {
+  app.whenReady().then(() => {
     checkForUpdate();
     autoCreateEditorWindows();
   });

programming_language: TypeScript
filename: library-files.js
change_type: ModificationType.MODIFY
diff: @@ -8,15 +8,6 @@ import {staticDir} from './environment';
 const readFile = promisify(fs.readFile);
 const decompress = promisify(brotliDecompress);
 
-protocol.registerSchemesAsPrivileged([
-  {
-    scheme: 'tw-library-files',
-    privileges: {
-      supportFetchAPI: true
-    }
-  }
-]);
-
 const mimeTypes = new Map();
 mimeTypes.set('wav', 'audio/wav');
 mimeTypes.set('svg', 'image/svg+xml');

programming_language: TypeScript
filename: protocols.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,16 @@
+import {protocol} from 'electron';
+
+protocol.registerSchemesAsPrivileged([
+  {
+    scheme: 'tw-extensions',
+    privileges: {
+      supportFetchAPI: true
+    }
+  },
+  {
+    scheme: 'tw-library-files',
+    privileges: {
+      supportFetchAPI: true
+    }
+  }
+]);

programming_language: TypeScript
filename: request-security.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,37 @@
+import {app} from 'electron';
+
+const isRequestAllowed = (details) => {
+  const url = new URL(details.url);
+  if (url.protocol === 'file:') {
+    // Requests to files must be inside the application
+    return url.pathname.startsWith(__dirname);
+  }
+
+  if (details.resourceType !== 'xhr') {
+    // Immune to CORS even in standard browsers
+    return true;
+  }
+
+  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
+    // Special protocols are immune to CORS
+    return true;
+  }
+
+  const allowOriginHeader = details.responseHeaders['access-control-allow-origin'];
+  if (!Array.isArray(allowOriginHeader)) {
+    // No header, not allowed
+    return false;
+  }
+
+  // Header must be set to "*"
+  // We don't have a real Origin that we can check
+  return allowOriginHeader.join(',') === '*';
+};
+
+app.on('session-created', (session) => {
+  session.webRequest.onHeadersReceived((details, callback) => {
+    callback({
+      cancel: !isRequestAllowed(details)
+    });
+  });
+});

programming_language: JavaScript
repo_url: https://github.com/TurboWarp/desktop
commit_details: repo_url: https://github.com/TurboWarp/desktop
author: GarboMuffin
author_date: 2023-01-24 23:47:59-06:00
msg: Remove the code that broke Windows
repository: repo_url: https://github.com/TurboWarp/desktop
repo_name: TurboWarp/desktop
description: A better offline editor for Scratch 3.
repo_language: JavaScript
owner: TurboWarp
file_changes: filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -22,7 +22,6 @@ import {isBackgroundThrottlingEnabled, whenBackgroundThrottlingChanged} from './
 import './extensions';
 import {createAtomicWriteStream} from './atomic-file-write-stream';
 import './protocols';
-import './request-security';
 
 const readFile = util.promisify(fs.readFile);
 const brotliDecompress = util.promisify(zlib.brotliDecompress);

programming_language: TypeScript
filename: request-security.js
change_type: ModificationType.DELETE
diff: @@ -1,37 +0,0 @@
-import {app} from 'electron';
-
-const isRequestAllowed = (details) => {
-  const url = new URL(details.url);
-  if (url.protocol === 'file:') {
-    // Requests to files must be inside the application
-    return url.pathname.startsWith(__dirname);
-  }
-
-  if (details.resourceType !== 'xhr') {
-    // Immune to CORS even in standard browsers
-    return true;
-  }
-
-  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
-    // Special protocols are immune to CORS
-    return true;
-  }
-
-  const allowOriginHeader = details.responseHeaders['access-control-allow-origin'];
-  if (!Array.isArray(allowOriginHeader)) {
-    // No header, not allowed
-    return false;
-  }
-
-  // Header must be set to "*"
-  // We don't have a real Origin that we can check
-  return allowOriginHeader.join(',') === '*';
-};
-
-app.on('session-created', (session) => {
-  session.webRequest.onHeadersReceived((details, callback) => {
-    callback({
-      cancel: !isRequestAllowed(details)
-    });
-  });
-});

programming_language: unknown