mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 00:09:30 +01:00
CVE-2026-21518 (MSRC#106249) (#32)
* strings: add a punycode encoding function Reuses existing unicode constructs we have. Verified against the RFC. Implemented with Opus, reviewed with Sonnet4.5, Gemini 3 Pro, and GPT 5.2. * mcp: explicitly request workspace trust to start MCP servers --------- Co-authored-by: Connor Peet <connor@peet.io>
This commit is contained in:
@@ -1409,3 +1409,179 @@ function toBinary(str: string): string {
|
||||
export function multibyteAwareBtoa(str: string): string {
|
||||
return btoa(toBinary(str));
|
||||
}
|
||||
|
||||
//#region Punycode
|
||||
|
||||
// RFC 3492 constants
|
||||
const enum PunycodeConstants {
|
||||
BASE = 36,
|
||||
TMIN = 1,
|
||||
TMAX = 26,
|
||||
SKEW = 38,
|
||||
DAMP = 700,
|
||||
INITIAL_BIAS = 72,
|
||||
INITIAL_N = 128,
|
||||
DELIMITER = CharCode.Dash
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a digit value (0-35) to its punycode character.
|
||||
* 0-25 -> 'a'-'z', 26-35 -> '0'-'9'
|
||||
*/
|
||||
function encodePunycodeDigit(d: number): number {
|
||||
return d < 26 ? CharCode.a + d : CharCode.Digit0 + d - 26;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts the bias according to RFC 3492 section 3.4.
|
||||
*/
|
||||
function adaptPunycodeBias(delta: number, numPoints: number, firstTime: boolean): number {
|
||||
delta = firstTime ? Math.floor(delta / PunycodeConstants.DAMP) : delta >> 1;
|
||||
delta += Math.floor(delta / numPoints);
|
||||
|
||||
let k = 0;
|
||||
const baseMinusTmin = PunycodeConstants.BASE - PunycodeConstants.TMIN;
|
||||
while (delta > (baseMinusTmin * PunycodeConstants.TMAX) >> 1) {
|
||||
delta = Math.floor(delta / baseMinusTmin);
|
||||
k += PunycodeConstants.BASE;
|
||||
}
|
||||
return k + Math.floor((baseMinusTmin + 1) * delta / (delta + PunycodeConstants.SKEW));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a Unicode string to Punycode according to RFC 3492.
|
||||
* This is the raw encoding without the "xn--" ACE prefix.
|
||||
*
|
||||
* @param input The Unicode string to encode.
|
||||
* @returns The Punycode-encoded ASCII string.
|
||||
* @throws Error if the input contains invalid surrogate pairs.
|
||||
*
|
||||
* @example
|
||||
* // allow-any-unicode-next-line
|
||||
* punycodeEncode('münchen') // returns 'mnchen-3ya'
|
||||
* // allow-any-unicode-next-line
|
||||
* punycodeEncode('bücher') // returns 'bcher-kva'
|
||||
* // allow-any-unicode-next-line
|
||||
* punycodeEncode('日本語') // returns 'wgv71a119e'
|
||||
*/
|
||||
export function punycodeEncode(input: string): string {
|
||||
const output: number[] = [];
|
||||
|
||||
// Collect all code points using the existing CodePointIterator
|
||||
const codePoints: number[] = [];
|
||||
const iterator = new CodePointIterator(input);
|
||||
while (!iterator.eol()) {
|
||||
const cp = iterator.nextCodePoint();
|
||||
// Check for lone surrogates (invalid Unicode)
|
||||
if (cp >= 0xD800 && cp <= 0xDFFF) {
|
||||
throw new Error('Invalid surrogate pair in input');
|
||||
}
|
||||
codePoints.push(cp);
|
||||
}
|
||||
|
||||
// Copy basic code points to output
|
||||
let basicCount = 0;
|
||||
for (const cp of codePoints) {
|
||||
if (cp < PunycodeConstants.INITIAL_N) {
|
||||
output.push(cp);
|
||||
basicCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Add delimiter if there were basic code points and there are non-basic ones
|
||||
const handledCount = basicCount;
|
||||
if (basicCount > 0 && basicCount < codePoints.length) {
|
||||
output.push(PunycodeConstants.DELIMITER);
|
||||
}
|
||||
|
||||
// Main encoding loop
|
||||
let n = PunycodeConstants.INITIAL_N;
|
||||
let delta = 0;
|
||||
let bias = PunycodeConstants.INITIAL_BIAS;
|
||||
let h = handledCount;
|
||||
|
||||
while (h < codePoints.length) {
|
||||
// Find the minimum code point >= n
|
||||
let m = 0x10FFFF; // Maximum valid Unicode code point
|
||||
for (const cp of codePoints) {
|
||||
if (cp >= n && cp < m) {
|
||||
m = cp;
|
||||
}
|
||||
}
|
||||
|
||||
// Increase delta to account for skipped code points
|
||||
const deltaIncrement = (m - n) * (h + 1);
|
||||
if (delta > Number.MAX_SAFE_INTEGER - deltaIncrement) {
|
||||
throw new Error('Punycode overflow');
|
||||
}
|
||||
delta += deltaIncrement;
|
||||
n = m;
|
||||
|
||||
// Process each code point
|
||||
for (const cp of codePoints) {
|
||||
if (cp < n) {
|
||||
delta++;
|
||||
if (delta === 0) {
|
||||
throw new Error('Punycode overflow');
|
||||
}
|
||||
} else if (cp === n) {
|
||||
// Encode delta as a variable-length integer
|
||||
let q = delta;
|
||||
for (let k = PunycodeConstants.BASE; ; k += PunycodeConstants.BASE) {
|
||||
const t = k <= bias ? PunycodeConstants.TMIN :
|
||||
k >= bias + PunycodeConstants.TMAX ? PunycodeConstants.TMAX :
|
||||
k - bias;
|
||||
if (q < t) {
|
||||
break;
|
||||
}
|
||||
const digit = t + ((q - t) % (PunycodeConstants.BASE - t));
|
||||
output.push(encodePunycodeDigit(digit));
|
||||
q = Math.floor((q - t) / (PunycodeConstants.BASE - t));
|
||||
}
|
||||
output.push(encodePunycodeDigit(q));
|
||||
bias = adaptPunycodeBias(delta, h + 1, h === handledCount);
|
||||
delta = 0;
|
||||
h++;
|
||||
}
|
||||
}
|
||||
delta++;
|
||||
n++;
|
||||
}
|
||||
|
||||
let ret = '';
|
||||
for (const ch of output) {
|
||||
ret += String.fromCharCode(ch);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a domain label using Punycode with the ACE prefix "xn--".
|
||||
* If the label contains only ASCII characters, it is returned unchanged.
|
||||
*
|
||||
* @param label The domain label to encode.
|
||||
* @returns The ACE-encoded label with "xn--" prefix, or the original label if all ASCII.
|
||||
*
|
||||
* @example
|
||||
* // allow-any-unicode-next-line
|
||||
* toPunycodeACE('münchen') // returns 'xn--mnchen-3ya'
|
||||
* toPunycodeACE('example') // returns 'example' (no encoding needed)
|
||||
*/
|
||||
export function toPunycodeACE(label: string): string {
|
||||
// Check if the label contains only ASCII characters
|
||||
let hasNonASCII = false;
|
||||
for (let i = 0; i < label.length; i++) {
|
||||
if (label.charCodeAt(i) >= PunycodeConstants.INITIAL_N) {
|
||||
hasNonASCII = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasNonASCII) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return 'xn--' + punycodeEncode(label);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -755,6 +755,106 @@ suite('Strings', () => {
|
||||
assert.ok(strings.multibyteAwareBtoa(new Array(100000).fill('vs').join('')).length > 0); // https://github.com/microsoft/vscode/issues/112013
|
||||
});
|
||||
|
||||
suite('punycode', () => {
|
||||
test('punycodeEncode - basic ASCII only', () => {
|
||||
// Pure ASCII strings should be returned as-is
|
||||
assert.strictEqual(strings.punycodeEncode('abc'), 'abc');
|
||||
assert.strictEqual(strings.punycodeEncode('hello'), 'hello');
|
||||
assert.strictEqual(strings.punycodeEncode('example'), 'example');
|
||||
});
|
||||
|
||||
test('punycodeEncode - empty string', () => {
|
||||
assert.strictEqual(strings.punycodeEncode(''), '');
|
||||
});
|
||||
|
||||
test('punycodeEncode - German words', () => {
|
||||
// "münchen" -> "mnchen-3ya"
|
||||
assert.strictEqual(strings.punycodeEncode('münchen'), 'mnchen-3ya');
|
||||
// "bücher" -> "bcher-kva"
|
||||
assert.strictEqual(strings.punycodeEncode('bücher'), 'bcher-kva');
|
||||
});
|
||||
|
||||
test('punycodeEncode - Chinese', () => {
|
||||
// "中文" -> "fiq228c"
|
||||
assert.strictEqual(strings.punycodeEncode('中文'), 'fiq228c');
|
||||
});
|
||||
|
||||
test('punycodeEncode - Japanese', () => {
|
||||
// "日本語" -> "wgv71a119e"
|
||||
assert.strictEqual(strings.punycodeEncode('日本語'), 'wgv71a119e');
|
||||
});
|
||||
|
||||
test('punycodeEncode - Arabic', () => {
|
||||
// RFC 3492 example (A) - Arabic (Egyptian)
|
||||
assert.strictEqual(
|
||||
strings.punycodeEncode('ليهمابتكلموشعربي؟'),
|
||||
'egbpdaj6bu4bxfgehfvwxn'
|
||||
);
|
||||
});
|
||||
|
||||
test('punycodeEncode - mixed ASCII and non-ASCII', () => {
|
||||
// "café" -> "caf-dma"
|
||||
assert.strictEqual(strings.punycodeEncode('café'), 'caf-dma');
|
||||
});
|
||||
|
||||
test('punycodeEncode - supplementary plane characters', () => {
|
||||
// Emoji test - "💻" (U+1F4BB = 128187 decimal)
|
||||
assert.strictEqual(strings.punycodeEncode('💻'), '3s8h');
|
||||
// Mixed with ASCII - "a💻b"
|
||||
assert.strictEqual(strings.punycodeEncode('a💻b'), 'ab-sv72a');
|
||||
});
|
||||
|
||||
test('punycodeEncode - RFC 3492 test vectors', () => {
|
||||
// (B) Chinese (simplified) - "他们为什么不说中文"
|
||||
assert.strictEqual(
|
||||
strings.punycodeEncode('\u4ed6\u4eec\u4e3a\u4ec0\u4e48\u4e0d\u8bf4\u4e2d\u6587'),
|
||||
'ihqwcrb4cv8a8dqg056pqjye'
|
||||
);
|
||||
|
||||
// (C) Chinese (traditional) - "他們爲什麽不說中文"
|
||||
assert.strictEqual(
|
||||
strings.punycodeEncode('\u4ed6\u5011\u7232\u4ec0\u9ebd\u4e0d\u8aaa\u4e2d\u6587'),
|
||||
'ihqwctvzc91f659drss3x8bo0yb'
|
||||
);
|
||||
|
||||
// (D) Czech - "Pročprostěnemluvíčesky" - Note: uppercase P is preserved
|
||||
assert.strictEqual(
|
||||
strings.punycodeEncode('Pro\u010dprost\u011bnemluv\u00ed\u010desky'),
|
||||
'Proprostnemluvesky-uyb24dma41a'
|
||||
);
|
||||
|
||||
// (L) Japanese - "3年B組金八先生" (3nen B gumi Kinpachi sensei)
|
||||
// Note: uppercase B is preserved in output
|
||||
assert.strictEqual(
|
||||
strings.punycodeEncode('3\u5e74B\u7d44\u91d1\u516b\u5148\u751f'),
|
||||
'3B-ww4c5e180e575a65lsy2b'
|
||||
);
|
||||
|
||||
// (M) Japanese - "安室奈美恵-with-SUPER-MONKEYS"
|
||||
// Note: ASCII characters preserve their original case
|
||||
assert.strictEqual(
|
||||
strings.punycodeEncode('\u5b89\u5ba4\u5948\u7f8e\u6075-with-SUPER-MONKEYS'),
|
||||
'-with-SUPER-MONKEYS-pc58ag80a8qai00g7n9n'
|
||||
);
|
||||
});
|
||||
|
||||
test('toACE - returns ASCII unchanged', () => {
|
||||
assert.strictEqual(strings.toPunycodeACE('example'), 'example');
|
||||
assert.strictEqual(strings.toPunycodeACE('hello-world'), 'hello-world');
|
||||
assert.strictEqual(strings.toPunycodeACE('test123'), 'test123');
|
||||
});
|
||||
|
||||
test('toACE - adds xn-- prefix for non-ASCII', () => {
|
||||
assert.strictEqual(strings.toPunycodeACE('münchen'), 'xn--mnchen-3ya');
|
||||
assert.strictEqual(strings.toPunycodeACE('bücher'), 'xn--bcher-kva');
|
||||
assert.strictEqual(strings.toPunycodeACE('日本語'), 'xn--wgv71a119e');
|
||||
});
|
||||
|
||||
test('toACE - empty string', () => {
|
||||
assert.strictEqual(strings.toPunycodeACE(''), '');
|
||||
});
|
||||
});
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { observableConfigValue } from '../../../../platform/observable/common/pl
|
||||
import { IQuickInputButton, IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';
|
||||
import { StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js';
|
||||
import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js';
|
||||
import { ConfigurationResolverExpression, IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js';
|
||||
import { AUX_WINDOW_GROUP, IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
@@ -85,6 +86,8 @@ export class McpRegistry extends Disposable implements IMcpRegistry {
|
||||
@IQuickInputService private readonly _quickInputService: IQuickInputService,
|
||||
@ILabelService private readonly _labelService: ILabelService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService,
|
||||
@IWorkspaceTrustRequestService private readonly _workspaceTrustRequestService: IWorkspaceTrustRequestService,
|
||||
) {
|
||||
super();
|
||||
this._mcpAccessValue = observableConfigValue(mcpAccessConfig, McpAccessValue.All, configurationService);
|
||||
@@ -213,6 +216,14 @@ export class McpRegistry extends Disposable implements IMcpRegistry {
|
||||
autoTrustChanges = false,
|
||||
errorOnUserInteraction = false,
|
||||
}: IMcpResolveConnectionOptions) {
|
||||
if (collection.scope === StorageScope.WORKSPACE && !this._workspaceTrustManagementService.isWorkspaceTrusted()) {
|
||||
if (errorOnUserInteraction) {
|
||||
throw new UserInteractionRequiredError('workspaceTrust');
|
||||
} else if (!await this._workspaceTrustRequestService.requestWorkspaceTrust({ message: localize('runTrust', "This MCP server definition is defined in your workspace files.") })) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (collection.trustBehavior === McpServerTrust.Kind.Trusted) {
|
||||
this._logService.trace(`MCP server ${definition.id} is trusted, no trust prompt needed`);
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user