Merge branch 'main' into tsgo-port

This commit is contained in:
Jake Bailey
2026-04-16 19:22:56 -07:00
11 changed files with 504 additions and 24 deletions

View File

@@ -327,7 +327,8 @@ export const enum NameValidationResult {
NameTooLong,
NameStartsWithDot,
NameStartsWithUnderscore,
NameContainsNonURISafeCharacters,
NameContainsInvalidCharacters,
NameContainsNonURISafeCharacters = NameContainsInvalidCharacters, // for backward compatibility
}
const maxPackageNameLength = 214;
@@ -381,8 +382,8 @@ function validatePackageNameWorker(packageName: string, supportScopedPackage: bo
return NameValidationResult.Ok;
}
}
if (encodeURIComponent(packageName) !== packageName) {
return NameValidationResult.NameContainsNonURISafeCharacters;
if (!/^[\w.-]+$/.test(packageName)) {
return NameValidationResult.NameContainsInvalidCharacters;
}
return NameValidationResult.Ok;
}
@@ -405,8 +406,8 @@ function renderPackageNameValidationFailureWorker(typing: string, result: NameVa
return `'${typing}':: ${kind} name '${name}' cannot start with '.'`;
case NameValidationResult.NameStartsWithUnderscore:
return `'${typing}':: ${kind} name '${name}' cannot start with '_'`;
case NameValidationResult.NameContainsNonURISafeCharacters:
return `'${typing}':: ${kind} name '${name}' contains non URI safe characters`;
case NameValidationResult.NameContainsInvalidCharacters:
return `'${typing}':: ${kind} name '${name}' contains invalid characters`;
case NameValidationResult.Ok:
return Debug.fail(); // Shouldn't have called this.
default:

4
src/lib/es5.d.ts vendored
View File

@@ -400,8 +400,8 @@ interface String {
charAt(pos: number): string;
/**
* Returns the Unicode value of the character at the specified location.
* @param index The zero-based index of the desired character. If there is no character at the specified index, NaN is returned.
* Returns the Unicode value of the character at the specified location, or NaN if the index is out of bounds.
* @param index The zero-based index of the desired character.
*/
charCodeAt(index: number): number;

View File

@@ -56,4 +56,24 @@ describe("unittests:: tsserver:: codeFix::", () => {
});
baselineTsserverLogs("codeFix", "install package when serialized", session);
});
it("install package rejects invalid package names", () => {
const { host, session } = setup();
// A client could craft an applyCodeActionCommand with arbitrary package names.
// The server must validate and reject names with invalid characters to prevent shell injection.
for (const packageName of ["; echo 'hello' #", "react'test", "a/b/c"]) {
session.executeCommandSeq<ts.server.protocol.ApplyCodeActionCommandRequest>({
command: ts.server.protocol.CommandTypes.ApplyCodeActionCommand,
arguments: {
command: {
type: "install package",
file: "/home/src/projects/project/src/file.ts",
packageName,
},
},
});
}
host.runPendingInstalls();
baselineTsserverLogs("codeFix", "install package rejects invalid package names", session);
});
});

View File

@@ -1524,10 +1524,11 @@ describe("unittests:: tsserver:: typingsInstaller:: Validate package name:", ()
it("package name cannot start with underscore", () => {
assert.equal(validatePackageName("_foo"), NameValidationResult.NameStartsWithUnderscore);
});
it("package non URI safe characters are not supported", () => {
assert.equal(validatePackageName(" scope "), NameValidationResult.NameContainsNonURISafeCharacters);
assert.equal(validatePackageName("; say Hello from TypeScript! #"), NameValidationResult.NameContainsNonURISafeCharacters);
assert.equal(validatePackageName("a/b/c"), NameValidationResult.NameContainsNonURISafeCharacters);
it("package invalid characters are not supported", () => {
assert.equal(validatePackageName(" scope "), NameValidationResult.NameContainsInvalidCharacters);
assert.equal(validatePackageName("; say Hello from TypeScript! #"), NameValidationResult.NameContainsInvalidCharacters);
assert.equal(validatePackageName("a/b/c"), NameValidationResult.NameContainsInvalidCharacters);
assert.equal(validatePackageName("react'test"), NameValidationResult.NameContainsInvalidCharacters);
});
it("scoped package name is supported", () => {
assert.equal(validatePackageName("@scope/bar"), NameValidationResult.Ok);
@@ -1540,10 +1541,10 @@ describe("unittests:: tsserver:: typingsInstaller:: Validate package name:", ()
assert.deepEqual(validatePackageName("@_scope/bar"), { name: "_scope", isScopeName: true, result: NameValidationResult.NameStartsWithUnderscore });
assert.deepEqual(validatePackageName("@_scope/_bar"), { name: "_scope", isScopeName: true, result: NameValidationResult.NameStartsWithUnderscore });
});
it("scope name in scoped package name with non URI safe characters are not supported", () => {
assert.deepEqual(validatePackageName("@ scope /bar"), { name: " scope ", isScopeName: true, result: NameValidationResult.NameContainsNonURISafeCharacters });
assert.deepEqual(validatePackageName("@; say Hello from TypeScript! #/bar"), { name: "; say Hello from TypeScript! #", isScopeName: true, result: NameValidationResult.NameContainsNonURISafeCharacters });
assert.deepEqual(validatePackageName("@ scope / bar "), { name: " scope ", isScopeName: true, result: NameValidationResult.NameContainsNonURISafeCharacters });
it("scope name in scoped package name with invalid characters are not supported", () => {
assert.deepEqual(validatePackageName("@ scope /bar"), { name: " scope ", isScopeName: true, result: NameValidationResult.NameContainsInvalidCharacters });
assert.deepEqual(validatePackageName("@; say Hello from TypeScript! #/bar"), { name: "; say Hello from TypeScript! #", isScopeName: true, result: NameValidationResult.NameContainsInvalidCharacters });
assert.deepEqual(validatePackageName("@ scope / bar "), { name: " scope ", isScopeName: true, result: NameValidationResult.NameContainsInvalidCharacters });
});
it("package name in scoped package name cannot start with dot", () => {
assert.deepEqual(validatePackageName("@scope/.bar"), { name: ".bar", isScopeName: false, result: NameValidationResult.NameStartsWithDot });
@@ -1551,9 +1552,9 @@ describe("unittests:: tsserver:: typingsInstaller:: Validate package name:", ()
it("package name in scoped package name cannot start with underscore", () => {
assert.deepEqual(validatePackageName("@scope/_bar"), { name: "_bar", isScopeName: false, result: NameValidationResult.NameStartsWithUnderscore });
});
it("package name in scoped package name with non URI safe characters are not supported", () => {
assert.deepEqual(validatePackageName("@scope/ bar "), { name: " bar ", isScopeName: false, result: NameValidationResult.NameContainsNonURISafeCharacters });
assert.deepEqual(validatePackageName("@scope/; say Hello from TypeScript! #"), { name: "; say Hello from TypeScript! #", isScopeName: false, result: NameValidationResult.NameContainsNonURISafeCharacters });
it("package name in scoped package name with invalid characters are not supported", () => {
assert.deepEqual(validatePackageName("@scope/ bar "), { name: " bar ", isScopeName: false, result: NameValidationResult.NameContainsInvalidCharacters });
assert.deepEqual(validatePackageName("@scope/; say Hello from TypeScript! #"), { name: "; say Hello from TypeScript! #", isScopeName: false, result: NameValidationResult.NameContainsInvalidCharacters });
});
});

View File

@@ -239,6 +239,22 @@ export abstract class TypingsInstaller {
/** @internal */
installPackage(req: InstallPackageRequest): void {
const { fileName, packageName, projectName, projectRootPath, id } = req;
const validationResult = JsTyping.validatePackageName(packageName);
if (validationResult !== JsTyping.NameValidationResult.Ok) {
const message = JsTyping.renderPackageNameValidationFailure(validationResult, packageName);
if (this.log.isEnabled()) {
this.log.writeLine(message);
}
const response: PackageInstalledResponse = {
kind: ActionPackageInstalled,
projectName,
id,
success: false,
message,
};
this.sendResponse(response);
return;
}
const cwd = forEachAncestorDirectory(getDirectoryPath(fileName), directory => {
if (this.installTypingHost.fileExists(combinePaths(directory, "package.json"))) {
return directory;