CVE-2024-22421 (TypeScript, published 2024-01-19T21:15Z)
Description: value: JupyterLab is an extensible environment for interactive and reproducible computing, based on the Jupyter Notebook and Architecture. Users of JupyterLab who click on a malicious link may get their `Authorization` and `XSRFToken` tokens exposed to a third party when running an older `jupyter-server` version. JupyterLab versions 4.1.0b2, 4.0.11, and 3.6.7 are patched. No workaround has been identified, however users should ensure to upgrade `jupyter-server` to version 2.7.2 or newer which includes a redirect vulnerability fix.
Severity: nan (CVSS v3 base score: 6.5)
Weakness: CWE-23 \u2014 Relative Path Traversal
The product uses external input to construct a pathname that should be within a restricted directory, but it does not properly neutralize sequences such as ".." that can resolve to a location that is outside of that directory.
Repository: https://github.com/jupyterlab/jupyterlab
Files changed:
packages/apputils-extension/src/workspacesplugin.ts
packages/hub-extension/src/index.ts
packages/services/src/session/restapi.ts
packages/services/src/setting/index.ts
packages/services/src/terminal/restapi.ts
packages/services/src/workspace/index.ts
packages/services/test/session/session.spec.ts
packages/services/test/setting/manager.spec.ts
packages/services/test/workspace/manager.spec.ts
packages/translation/src/server.ts
Commit message: Merge pull request from GHSA-44cc-43rp-5947

Co-authored-by: Frédéric Collonval <fcollonval@users.noreply.github.com>

Vulnerable code:
        const url = URLExt.join(this._application, 'workspaces', id);
  const restartUrl = hubServerName
    ? hubHost + URLExt.join(hubPrefix, 'spawn', hubUser, hubServerName)
    : hubHost + URLExt.join(hubPrefix, 'spawn');
  return URLExt.join(baseUrl, SESSION_SERVICE_URL, id);
    return `${URLExt.join(base, SERVICE_SETTINGS_URL, id)}${idsOnlyParam}`;
  const url = URLExt.join(settings.baseUrl, TERMINAL_SERVICE_URL, name);
    return URLExt.join(base, SERVICE_WORKSPACES_URL, id);
  const requestUrl = URLExt.join(settings.baseUrl, translationsUrl, locale);

Fixed code:
        const workspacesBase = URLExt.join(this._application, 'workspaces');
        const url = URLExt.join(workspacesBase, id);
        if (!workspacesBase.startsWith(url)) {
          throw new Error('Can only be used for workspaces');
        }
  const spawnBase = URLExt.join(hubPrefix, 'spawn');
  let restartUrl: string;
  if (hubServerName) {
    const suffix = URLExt.join(spawnBase, hubUser, hubServerName);
    if (!suffix.startsWith(spawnBase)) {
      throw new Error('Can only be used for spawn requests');
    }
    restartUrl = hubHost + suffix;
  }
  restartUrl = hubHost + spawnBase;
  const servicesBase = URLExt.join(baseUrl, SESSION_SERVICE_URL);
  const result = URLExt.join(servicesBase, id);
  if (!result.startsWith(servicesBase)) {
    throw new Error('Can only be used for services requests');
  }
  return result;
    const settingsBase = URLExt.join(base, SERVICE_SETTINGS_URL);
    const result = URLExt.join(settingsBase, id);
    if (!result.startsWith(settingsBase)) {
      throw new Error('Can only be used for workspaces requests');
    }
    return `${result}${idsOnlyParam}`;
  const workspacesBase = URLExt.join(settings.baseUrl, TERMINAL_SERVICE_URL);
  const url = URLExt.join(workspacesBase, name);
  if (!url.startsWith(workspacesBase)) {
    throw new Error('Can only be used for terminal requests');
  }
    const workspacesBase = URLExt.join(base, SERVICE_WORKSPACES_URL);
    const result = URLExt.join(workspacesBase, id);
    if (!result.startsWith(workspacesBase)) {
      throw new Error('Can only be used for workspaces requests');
    }
    return result;

    it('should reject invalid on invalid id', async () => {
      await expect(SessionAPI.shutdownSession('../')).rejects.toThrow();
    });

      it('should reject on invalid id', async () => {
        const id = '../';

        const callback = async () => {
          await manager.fetch(id);
        };
        await expect(callback).rejects.toThrow();
      });

      it('should reject on invalid id', async () => {
        const id = '../';
        const theme = 'Foo Theme';
        const raw = `{"theme": "${theme}"}`;

        const callback = async () => {
          await manager.save(id, raw);
        };
        await expect(callback).rejects.toThrow();
      });

      it('should reject on invalid id', async () => {
        const id = '../';

        const callback = async () => {
          await manager.fetch(id);
        };
        await expect(callback).rejects.toThrow();
      });

      it('should reject on invalid id', async () => {
        const id = '../';

        const callback = async () => {
          await manager.save(id, { data: {}, metadata: { id } });
        };
        await expect(callback).rejects.toThrow();
      });
  const translationsBase = URLExt.join(settings.baseUrl, translationsUrl);
  const requestUrl = URLExt.join(translationsBase, locale);
  if (!requestUrl.startsWith(translationsBase)) {
    throw new Error('Can only be used for translations requests');
  }