CVE-2023-40168 (JavaScript, published 2023-08-17T20:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: 6.5)
Weakness: CWE-863 \u2014 Incorrect Authorization
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.
Repository: https://github.com/TurboWarp/desktop
Files changed:
src/l10n/en.json
src/main/bypass-cors.js
src/main/extensions.js
src/main/index.js
src/renderer/desktop-settings/desktop-settings.jsx
Commit message: Rewrite CORS enforcement and offline extension cache

This fixes the offline extension cache.
Also added an option to force CORS to be enabled.


Vulnerable code:
    urls: ['https://extensions.turbowarp.org/*']
    const path = new URL(details.url).pathname;
    callback({
      redirectURL: `tw-extensions://${path}`
    });

  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({});
  });

      isBackgroundThrottlingEnabled: ipcRenderer.sendSync('background-throttling/get-is-enabled')

Fixed code:
  "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.",
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
};
import {canBypassCORS} from './bypass-cors';
  const rootFileURL = new URL(`file://${__dirname}/`).href;

  // We can only use one onBeforeRequest -- calling it again removes the previous listener.
    urls: ['file://*', 'https://extensions.turbowarp.org/*']
    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({});
      isBackgroundThrottlingEnabled: ipcRenderer.sendSync('background-throttling/get-is-enabled'),
      canBypassCORS: ipcRenderer.sendSync('bypass-cors/get-is-enabled')
    this.handleBypassCORSChanged = this.handleBypassCORSChanged.bind(this);
  handleBypassCORSChanged (e) {
    const enabled = e.target.checked;
    ipcRenderer.invoke('bypass-cors/set-is-enabled', enabled);
    this.setState({
      canBypassCORS: enabled
    });
  }

        <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>