CVE-2021-28793 (published 2021-04-20T13:15Z)

vscode-restructuredtext before 146.0.0 contains an incorrect access control vulnerability, where a crafted project folder could execute arbitrary binaries via crafted workspace configuration.

Severity: HIGH
CVSS v2 base score: 7.5 (AV:N/AC:L/Au:N/C:P/I:P/A:P)
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-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/vscode-restructuredtext/vscode-restructuredtext
commit_details: repo_url: https://github.com/vscode-restructuredtext/vscode-restructuredtext
author: Lex Li
author_date: 2021-04-05 18:39:11-04:00
msg: Patch to address workspace security issue.
repository: repo_url: https://github.com/vscode-restructuredtext/vscode-restructuredtext
repo_name: vscode-restructuredtext/vscode-restructuredtext
description: reStructuredText Language Support in Visual Studio Code
repo_language: TypeScript
owner: vscode-restructuredtext
file_changes: filename: package-lock.json
change_type: ModificationType.MODIFY
diff: @@ -1287,18 +1287,32 @@
       }
     },
     "elliptic": {
-      "version": "6.5.3",
-      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
-      "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
+      "version": "6.5.4",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
+      "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
       "dev": true,
       "requires": {
-        "bn.js": "^4.4.0",
-        "brorand": "^1.0.1",
+        "bn.js": "^4.11.9",
+        "brorand": "^1.1.0",
         "hash.js": "^1.0.0",
-        "hmac-drbg": "^1.0.0",
-        "inherits": "^2.0.1",
-        "minimalistic-assert": "^1.0.0",
-        "minimalistic-crypto-utils": "^1.0.0"
+        "hmac-drbg": "^1.0.1",
+        "inherits": "^2.0.4",
+        "minimalistic-assert": "^1.0.1",
+        "minimalistic-crypto-utils": "^1.0.1"
+      },
+      "dependencies": {
+        "bn.js": {
+          "version": "4.12.0",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+          "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+          "dev": true
+        },
+        "inherits": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+          "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+          "dev": true
+        }
       }
     },
     "emoji-regex": {

programming_language: TypeScript
filename: package.json
change_type: ModificationType.MODIFY
diff: @@ -240,6 +240,11 @@
         "command": "restructuredtext.preview.toggleLock",
         "title": "%restructuredtext.preview.toggleLock.title%",
         "category": "reStructuredText"
+      },
+      {
+        "command": "restructuredtext.workspace.isTrusted.toggle",
+        "title": "reStructuredText: Toggle Workspace Trust Flag",
+        "description": "Toggle the workspace trust flag. Workspace settings that determine tool locations are disabled by default in untrusted workspaces."
       }
     ],
     "menus": {

programming_language: CoffeeScript
filename: config.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,151 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/*---------------------------------------------------------
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+import vscode = require('vscode');
+import { getFromWorkspaceState, updateWorkspaceState } from './stateUtils';
+
+const WORKSPACE_IS_TRUSTED_KEY = 'WORKSPACE_IS_TRUSTED_KEY';
+const SECURITY_SENSITIVE_CONFIG: string[] = [
+    'sphinxBuildPath',
+    'linter.executablePath'
+];
+
+export async function initConfig(ctx: vscode.ExtensionContext) {
+    const isTrusted = getFromWorkspaceState(WORKSPACE_IS_TRUSTED_KEY, false);
+    if (isTrusted !== defaultConfig.workspaceIsTrusted()) {
+        defaultConfig.toggleWorkspaceIsTrusted();
+    }
+    ctx.subscriptions.push(vscode.commands.registerCommand('restructuredtext.workspace.isTrusted.toggle', toggleWorkspaceIsTrusted));
+
+    if (isTrusted) {
+        return;
+    }
+    const ignored = ignoredWorkspaceConfig(vscode.workspace.getConfiguration('restructuredtext'), SECURITY_SENSITIVE_CONFIG);
+    if (ignored.length === 0) {
+        return;
+    }
+    const ignoredSettings = ignored.map((x) => `"restructuredtext.${x}"`).join(',');
+    const val = await vscode.window.showWarningMessage(
+        `Some workspace/folder-level settings (${ignoredSettings}) from the untrusted workspace are disabled ` +
+        'by default. If this workspace is trusted, explicitly enable the workspace/folder-level settings ' +
+        'by running the "reStructuredText: Toggle Workspace Trust Flag" command.',
+        'OK',
+        'Trust This Workspace',
+        'More Info'
+    );
+    switch (val) {
+        case 'Trust This Workspace':
+            await toggleWorkspaceIsTrusted();
+            break;
+        case 'More Info':
+            vscode.env.openExternal(
+                vscode.Uri.parse('https://docs.restructuredtext.net/articles/configuration.html#security')
+            );
+            break;
+        default:
+            break;
+    }
+}
+
+function ignoredWorkspaceConfig(cfg: vscode.WorkspaceConfiguration, keys: string[]) {
+    return keys.filter((key) => {
+        const inspect = cfg.inspect(key);
+        return inspect.workspaceValue !== undefined || inspect.workspaceFolderValue !== undefined;
+    });
+}
+
+async function toggleWorkspaceIsTrusted() {
+    const v = defaultConfig.toggleWorkspaceIsTrusted();
+    await updateWorkspaceState(WORKSPACE_IS_TRUSTED_KEY, v);
+}
+
+// reStructuredText extension configuration for a workspace.
+export class Configuration {
+    constructor(private _workspaceIsTrusted = false, private getConfiguration = vscode.workspace.getConfiguration) { }
+
+    public toggleWorkspaceIsTrusted() {
+        this._workspaceIsTrusted = !this._workspaceIsTrusted;
+        return this._workspaceIsTrusted;
+    }
+
+    // returns a Proxied vscode.WorkspaceConfiguration, which prevents
+    // from using the workspace configuration if the workspace is untrusted.
+    public get(section: string, uri?: vscode.Uri): vscode.WorkspaceConfiguration {
+        const cfg = this.getConfiguration(section, uri);
+        if (section !== 'restructuredtext' || this._workspaceIsTrusted) {
+            return cfg;
+        }
+        return new WrappedConfiguration(cfg);
+    }
+
+    public workspaceIsTrusted(): boolean {
+        return this._workspaceIsTrusted;
+    }
+}
+
+const defaultConfig = new Configuration();
+
+// Returns the workspace Configuration used by the extension.
+export function DefaultConfig() {
+    return defaultConfig;
+}
+
+// wrappedConfiguration wraps vscode.WorkspaceConfiguration.
+// tslint:disable-next-line: max-classes-per-file
+class WrappedConfiguration implements vscode.WorkspaceConfiguration {
+    constructor(private readonly _wrapped: vscode.WorkspaceConfiguration) {
+        // set getters for direct setting access (e.g. cfg.gopath), but don't overwrite _wrapped.
+        const desc = Object.getOwnPropertyDescriptors(_wrapped);
+        for (const prop in desc) {
+            // TODO(hyangah): find a better way to exclude WrappedConfiguration's members.
+            // These methods are defined by WrappedConfiguration.
+            if (typeof prop === 'string' && !['get', 'has', 'inspect', 'update', '_wrapped'].includes(prop)) {
+                const d = desc[prop];
+                if (SECURITY_SENSITIVE_CONFIG.includes(prop)) {
+                    const inspect = this._wrapped.inspect(prop);
+                    d.value = inspect.globalValue ?? inspect.defaultValue;
+                }
+                Object.defineProperty(this, prop, desc[prop]);
+            }
+        }
+    }
+
+    public get(section: any, defaultValue?: any) {
+        if (SECURITY_SENSITIVE_CONFIG.includes(section)) {
+            const inspect = this._wrapped.inspect(section);
+            return inspect.globalValue ?? defaultValue ?? inspect.defaultValue;
+        }
+        return this._wrapped.get(section, defaultValue);
+    }
+    public has(section: string) {
+        return this._wrapped.has(section);
+    }
+    public inspect<T>(section: string) {
+        return this._wrapped.inspect<T>(section);
+    }
+    public update(
+        section: string,
+        value: any,
+        configurationTarget?: boolean | vscode.ConfigurationTarget,
+        overrideInLanguage?: boolean
+    ): Thenable<void> {
+        return this._wrapped.update(section, value, configurationTarget, overrideInLanguage);
+    }
+}
+
+export function getConfig(section: string, uri?: vscode.Uri) {
+    if (!uri) {
+        if (vscode.window.activeTextEditor) {
+            uri = vscode.window.activeTextEditor.document.uri;
+        } else {
+            uri = null;
+        }
+    }
+    return defaultConfig.get(section, uri);
+}
+
+// True if the extension is running in known cloud-based IDEs.
+export const IsInCloudIDE = process.env.CLOUD_SHELL === 'true' || process.env.CODESPACES === 'true';

programming_language: TypeScript
filename: extension.ts
change_type: ModificationType.MODIFY
diff: @@ -9,56 +9,64 @@ import * as commands from './commands/index';
 import { RSTContentProvider } from './features/previewContentProvider';
 import { RSTPreviewManager } from './features/previewManager';
 import { Logger } from './logger';
-import { ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './security';
 import { Python } from './python';
 import { RSTEngine } from './rstEngine';
+import { ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './security';
 
 import * as listEditing from './features/listEditing';
+import { rstDocumentSymbolProvider } from './features/rstDocumentSymbolProvider';
 import RstLintingProvider from './features/rstLinter';
 import { underline } from './features/underline';
 import { Configuration } from './features/utils/configuration';
 import RstTransformerStatus from './features/utils/statusBar';
 import * as RstLanguageServer from './rstLsp/extension';
-import { rstDocumentSymbolProvider } from './features/rstDocumentSymbolProvider';
+import { setGlobalState, setWorkspaceState } from './stateUtils';
+import { initConfig } from './config';
 
-let extensionPath = "";
+let extensionPath = '';
 
 export function getExtensionPath(): string {
-	return extensionPath;
+    return extensionPath;
 }
 
 export async function activate(context: vscode.ExtensionContext): Promise<{ initializationFinished: Promise<void> }> {
-	extensionPath = context.extensionPath;
 
-	const logger = new Logger();
-	logger.log('Please visit https://docs.restructuredtext.net to learn how to configure the extension.');
+    setGlobalState(context.globalState);
+    setWorkspaceState(context.workspaceState);
+
+    await initConfig(context);
+
+    extensionPath = context.extensionPath;
 
-	const conflicting = Configuration.getConflictingExtensions();
-	for (const element of conflicting) {
-		const found = vscode.extensions.getExtension(element);
-		if (found) {
-			const message = `Found conflicting extension ${element}. Please uninstall it.`;
+    const logger = new Logger();
+    logger.log('Please visit https://docs.restructuredtext.net to learn how to configure the extension.');
+
+    const conflicting = Configuration.getConflictingExtensions();
+    for (const element of conflicting) {
+        const found = vscode.extensions.getExtension(element);
+        if (found) {
+            const message = `Found conflicting extension ${element}. Please uninstall it.`;
             logger.log(message);
-			vscode.window.showErrorMessage(message);
-		}
-	}
+            vscode.window.showErrorMessage(message);
+        }
+    }
 
     await logPlatform(logger);
-	const disableLsp = Configuration.getLanguageServerDisabled();
+    const disableLsp = Configuration.getLanguageServerDisabled();
 
     const python: Python = new Python(logger);
 
-	// activate language services
-	const rstLspPromise = RstLanguageServer.activate(context, logger, disableLsp, python);
+    // activate language services
+    const rstLspPromise = RstLanguageServer.activate(context, logger, disableLsp, python);
 
     // Section creation support.
-	context.subscriptions.push(
-		vscode.commands.registerTextEditorCommand('restructuredtext.features.underline.underline', underline),
-		vscode.commands.registerTextEditorCommand('restructuredtext.features.underline.underlineReverse',
-			(textEditor, edit) => underline(textEditor, edit, true)),
-	);
+    context.subscriptions.push(
+        vscode.commands.registerTextEditorCommand('restructuredtext.features.underline.underline', underline),
+        vscode.commands.registerTextEditorCommand('restructuredtext.features.underline.underlineReverse',
+            (textEditor, edit) => underline(textEditor, edit, true)),
+    );
 
-	// Linter support
+    // Linter support
     if (!Configuration.getLinterDisabled()) {
         const linter = new RstLintingProvider(logger, python);
         linter.activate(context.subscriptions);
@@ -76,7 +84,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<{ init
 
         vscode.window.onDidChangeActiveTextEditor(status.update, status, context.subscriptions);
         status.update();
-	
+
         const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState, context.workspaceState);
 
         const engine: RSTEngine = new RSTEngine(python, logger, status);
@@ -104,7 +112,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<{ init
             previewManager.updateConfiguration();
         }));
     }
-    
+
     // DocumentSymbolProvider Demo, for Outline View Test
     let disposableRstDSP = vscode.languages.registerDocumentSymbolProvider(
         { scheme: 'file', language: 'restructuredtext' }, new rstDocumentSymbolProvider()
@@ -113,23 +121,23 @@ export async function activate(context: vscode.ExtensionContext): Promise<{ init
 
     listEditing.activate(context);
 
-	return {
-		initializationFinished: Promise.all([rstLspPromise])
-			.then((promiseResult) => {
-				// This promise resolver simply swallows the result of Promise.all.
-				// When we decide we want to expose this level of detail
-				// to other extensions then we will design that return type and implement it here.
-			}),
-	};
+    return {
+        initializationFinished: Promise.all([rstLspPromise])
+            .then((promiseResult) => {
+                // This promise resolver simply swallows the result of Promise.all.
+                // When we decide we want to expose this level of detail
+                // to other extensions then we will design that return type and implement it here.
+            }),
+    };
 }
 
 async function logPlatform(logger: Logger): Promise<void> {
-	const os = require('os');
+    const os = require('os');
     let platform = os.platform();
     logger.log(`OS is ${platform}`);
-	if (platform === 'darwin' || platform === 'win32') {
-		return;
-	}
+    if (platform === 'darwin' || platform === 'win32') {
+        return;
+    }
 
     const osInfo = require('linux-os-info');
     const result = await osInfo();

programming_language: TypeScript
filename: configuration.ts
change_type: ModificationType.MODIFY
diff: @@ -1,10 +1,11 @@
 'use strict';
 
-import {
-    Uri, workspace, WorkspaceFolder, extensions, WorkspaceConfiguration
-} from 'vscode';
 import * as fs from 'fs';
 import * as path from 'path';
+import {
+    extensions, Uri, workspace, WorkspaceConfiguration, WorkspaceFolder
+} from 'vscode';
+import { getConfig } from '../../config';
 import { Constants } from './constants';
 
 export class Configuration {
@@ -154,13 +155,14 @@ export class Configuration {
     private static loadAnySetting<T>(
         configSection: string, defaultValue: T, resource: Uri, header: string = 'restructuredtext',
     ): T {
-        return workspace.getConfiguration(header, resource).get(configSection, defaultValue);
+        // return workspace.getConfiguration(header, resource).get(configSection, defaultValue);
+        return getConfig(header, resource).get(configSection, defaultValue);
     }
 
     private static async saveAnySetting<T>(
         configSection: string, value: T, resource: Uri, header: string = 'restructuredtext',
     ): Promise<T> {
-        await workspace.getConfiguration(header, resource).update(configSection, value);
+        await getConfig(header, resource).update(configSection, value);
         return value;
     }
 

programming_language: TypeScript
filename: stateUtils.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,91 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/*---------------------------------------------------------
+ * Copyright (C) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+import vscode = require('vscode');
+
+let globalState: vscode.Memento;
+let workspaceState: vscode.Memento;
+
+export function getFromGlobalState(key: string, defaultValue?: any): any {
+	if (!globalState) {
+		return defaultValue;
+	}
+	return globalState.get(key, defaultValue);
+}
+
+export function updateGlobalState(key: string, value: any) {
+	if (!globalState) {
+		return;
+	}
+	return globalState.update(key, value);
+}
+
+export function setGlobalState(state: vscode.Memento) {
+	globalState = state;
+}
+
+export function getGlobalState() {
+	return globalState;
+}
+
+export function resetGlobalState() {
+	resetStateQuickPick(globalState, updateGlobalState);
+}
+
+export function getFromWorkspaceState(key: string, defaultValue?: any) {
+	if (!workspaceState) {
+		return defaultValue;
+	}
+	return workspaceState.get(key, defaultValue);
+}
+
+export function updateWorkspaceState(key: string, value: any) {
+	if (!workspaceState) {
+		return;
+	}
+	return workspaceState.update(key, value);
+}
+
+export function setWorkspaceState(state: vscode.Memento) {
+	workspaceState = state;
+}
+
+export function getWorkspaceState(): vscode.Memento {
+	return workspaceState;
+}
+
+export function resetWorkspaceState() {
+	resetStateQuickPick(workspaceState, updateWorkspaceState);
+}
+
+export function getMementoKeys(state: vscode.Memento): string[] {
+	if (!state) {
+		return [];
+	}
+	// tslint:disable-next-line: no-empty
+	if ((state as any)._value) {
+		const keys = Object.keys((state as any)._value);
+		// Filter out keys with undefined values, so they are not shown
+		// in the quick pick menu.
+		return keys.filter((key) => state.get(key) !== undefined);
+	}
+	return [];
+}
+
+async function resetStateQuickPick(state: vscode.Memento, updateFn: (key: string, value: any) => {}) {
+	const items = await vscode.window.showQuickPick(getMementoKeys(state), {
+		canPickMany: true,
+		placeHolder: 'Select the keys to reset.'
+	});
+	resetItemsState(items, updateFn);
+}
+
+export function resetItemsState(items: string[], updateFn: (key: string, value: any) => {}) {
+	if (!items) {
+		return;
+	}
+	items.forEach((item) => updateFn(item, undefined));
+}

programming_language: TypeScript