CVE-2023-40168: Incorrect Authorization (CWE-863)
value: 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.
CWE 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.
CVSS v3 base score: 6.5
Published: 2023-08-17T20:15Z
Repository: https://github.com/TurboWarp/desktop
Language: JavaScript
Files changed: src/main/extensions.js, src/main/index.js, src/main/library-files.js, src/main/protocols.js, src/main/request-security.js
Commit date: 2023-01-21 01:14:50 -0600
Commit message: Fix some bugs related to protocols and file: URIs

Diff: diff --git a/src/main/extensions.js b/src/main/extensions.js
index be8dd43..dc61557 100644
--- a/src/main/extensions.js
+++ b/src/main/extensions.js
@@ -13,19 +13,10 @@ app.on('session-created', (session) => {
       redirectURL: `tw-extensions://${path}`
     });
   });
 });
 
-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);
     const path = pathAndQuery.split('?')[0];
     const staticPath = pathUtil.join(extensionDirectory, path);
diff --git a/src/main/index.js b/src/main/index.js
index e1d66a9..ea20705 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -19,10 +19,12 @@ import './get-debug-info';
 import {handlePermissionRequest} from './permissions';
 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);
 
 const filesToOpen = [];
@@ -881,11 +883,11 @@ if (acquiredLock) {
     if (app.isReady() && editorWindows.size === 0) {
       createEditorWindow();
     }
   });
 
-  app.on('ready', () => {
+  app.whenReady().then(() => {
     checkForUpdate();
     autoCreateEditorWindows();
   });
 } else {
   console.log('Handing off to existing instance.');
diff --git a/src/main/library-files.js b/src/main/library-files.js
index 5d00795..dcb45fe 100644
--- a/src/main/library-files.js
+++ b/src/main/library-files.js
@@ -6,19 +6,10 @@ import {brotliDecompress} from 'zlib';
 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');
 mimeTypes.set('png', 'image/png');
 
diff --git a/src/main/protocols.js b/src/main/protocols.js
new file mode 100644
index 0000000..23ede19
--- /dev/null
+++ b/src/main/protocols.js
@@ -0,0 +1,16 @@
+import {protocol} from 'electron';
+
+protocol.registerSchemesAsPrivileged([
+  {
+    scheme: 'tw-extensions',
+    privileges: {
+      supportFetchAPI: true
+    }
+  },
+  {
+    scheme: 'tw-library-files',
+    privileges: {
+      supportFetchAPI: true
+    }
+  }
+]);
diff --git a/src/main/request-security.js b/src/main/request-security.js
new file mode 100644
index 0000000..0471f77
--- /dev/null
+++ b/src/main/request-security.js
@@ -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)
+    });
+  });
+});

Vulnerable code: protocol.registerSchemesAsPrivileged([
  {
    scheme: 'tw-extensions',
    privileges: {
      supportFetchAPI: true
    }
  }
]);

  app.on('ready', () => {
protocol.registerSchemesAsPrivileged([
  {
    scheme: 'tw-library-files',
    privileges: {
      supportFetchAPI: true
    }
  }
]);

Fixed code: import './protocols';
import './request-security';
  app.whenReady().then(() => {
import {protocol} from 'electron';

protocol.registerSchemesAsPrivileged([
  {
    scheme: 'tw-extensions',
    privileges: {
      supportFetchAPI: true
    }
  },
  {
    scheme: 'tw-library-files',
    privileges: {
      supportFetchAPI: true
    }
  }
]);
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)
    });
  });
});