diff --git a/src/testRunner/unittests/tsserver/codeFix.ts b/src/testRunner/unittests/tsserver/codeFix.ts index 481441b7cd1b4..007d835ca48ba 100644 --- a/src/testRunner/unittests/tsserver/codeFix.ts +++ b/src/testRunner/unittests/tsserver/codeFix.ts @@ -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({ + 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); + }); }); diff --git a/src/typingsInstallerCore/typingsInstaller.ts b/src/typingsInstallerCore/typingsInstaller.ts index 25f583513a5fe..aaafeaa477441 100644 --- a/src/typingsInstallerCore/typingsInstaller.ts +++ b/src/typingsInstallerCore/typingsInstaller.ts @@ -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; diff --git a/tests/baselines/reference/tsserver/codeFix/install-package-rejects-invalid-package-names.js b/tests/baselines/reference/tsserver/codeFix/install-package-rejects-invalid-package-names.js new file mode 100644 index 0000000000000..14acc9d58c607 --- /dev/null +++ b/tests/baselines/reference/tsserver/codeFix/install-package-rejects-invalid-package-names.js @@ -0,0 +1,438 @@ +Info seq [hh:mm:ss:mss] currentDirectory:: /home/src/Vscode/Projects/bin useCaseSensitiveFileNames:: false +Info seq [hh:mm:ss:mss] libs Location:: /home/src/tslibs/TS/Lib +Info seq [hh:mm:ss:mss] globalTypingsCacheLocation:: /home/src/Library/Caches/typescript +Info seq [hh:mm:ss:mss] Provided types map file "/home/src/tslibs/TS/Lib/typesMap.json" doesn't exist +Before request +//// [/home/src/projects/project/src/file.ts] +import * as os from "os"; +import * as https from "https"; +import * as vscode from "vscode"; + + +//// [/home/src/projects/project/tsconfig.json] +{ } + +//// [/home/src/projects/project/node_modules/vscode/index.js] +export const x = 10; + +//// [/home/src/tslibs/TS/Lib/lib.d.ts] +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +declare const console: { log(msg: any): void; }; + + +Info seq [hh:mm:ss:mss] request: + { + "command": "open", + "arguments": { + "file": "/home/src/projects/project/src/file.ts" + }, + "seq": 1, + "type": "request" + } +Info seq [hh:mm:ss:mss] getConfigFileNameForFile:: File: /home/src/projects/project/src/file.ts ProjectRootPath: undefined:: Result: /home/src/projects/project/tsconfig.json +Info seq [hh:mm:ss:mss] Creating ConfiguredProject: /home/src/projects/project/tsconfig.json, currentDirectory: /home/src/projects/project +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /home/src/projects/project/tsconfig.json 2000 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] Config: /home/src/projects/project/tsconfig.json : { + "rootNames": [ + "/home/src/projects/project/src/file.ts" + ], + "options": { + "configFilePath": "/home/src/projects/project/tsconfig.json" + } +} +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "projectLoadingStart", + "body": { + "projectName": "/home/src/projects/project/tsconfig.json", + "reason": "Creating possible configured project for /home/src/projects/project/src/file.ts to open" + } + } +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/project 1 undefined Config: /home/src/projects/project/tsconfig.json WatchType: Wild card directory +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/project 1 undefined Config: /home/src/projects/project/tsconfig.json WatchType: Wild card directory +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /home/src/projects/project/tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/project/src 1 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/project/src 1 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /home/src/projects 0 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /home/src/projects 0 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/project/node_modules 1 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/project/node_modules 1 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/node_modules 1 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/node_modules 1 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/project 0 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /home/src/projects/project 0 undefined Project: /home/src/projects/project/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /home/src/projects/project/tsconfig.json projectStateVersion: 1 projectProgramVersion: 0 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/home/src/projects/project/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (2) + /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts Text-1 "interface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array { length: number; [n: number]: T; }\ninterface ReadonlyArray {}\ndeclare const console: { log(msg: any): void; };" + /home/src/projects/project/src/file.ts SVC-1-0 "import * as os from \"os\";\nimport * as https from \"https\";\nimport * as vscode from \"vscode\";\n" + + + ../../tslibs/TS/Lib/lib.es2025.full.d.ts + Default library for target 'es2025' + src/file.ts + Matched by default include pattern '**/*' + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "projectLoadingFinish", + "body": { + "projectName": "/home/src/projects/project/tsconfig.json" + } + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "telemetry", + "body": { + "telemetryEventName": "projectInfo", + "payload": { + "projectId": "1097a5f82e8323ba7aba7567ec06402f7ad4ea74abce44ec5efd223ac77ff169", + "fileStats": { + "js": 0, + "jsSize": 0, + "jsx": 0, + "jsxSize": 0, + "ts": 1, + "tsSize": 92, + "tsx": 0, + "tsxSize": 0, + "dts": 1, + "dtsSize": 374, + "deferred": 0, + "deferredSize": 0 + }, + "compilerOptions": {}, + "typeAcquisition": { + "enable": false, + "include": false, + "exclude": false + }, + "extends": false, + "files": false, + "include": false, + "exclude": false, + "compileOnSave": false, + "configFileName": "tsconfig.json", + "projectType": "configured", + "languageServiceEnabled": true, + "version": "FakeVersion" + } + } + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "configFileDiag", + "body": { + "triggerFile": "/home/src/projects/project/src/file.ts", + "configFile": "/home/src/projects/project/tsconfig.json", + "diagnostics": [] + } + } +Info seq [hh:mm:ss:mss] Project '/home/src/projects/project/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (2) + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /home/src/projects/project/src/file.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /home/src/projects/project/tsconfig.json +Info seq [hh:mm:ss:mss] response: + { + "seq": 0, + "type": "response", + "command": "open", + "request_seq": 1, + "success": true, + "performanceData": { + "updateGraphDurationMs": * + } + } +After request +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* + + +PolledWatches:: +/home/src/projects/node_modules: *new* + {"pollingInterval":500} + +FsWatches:: +/home/src/projects: *new* + {} +/home/src/projects/project: *new* + {} +/home/src/projects/project/tsconfig.json: *new* + {} +/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts: *new* + {} + +FsWatchesRecursive:: +/home/src/projects/project: *new* + {} +/home/src/projects/project/node_modules: *new* + {} +/home/src/projects/project/src: *new* + {} + +Projects:: +/home/src/projects/project/tsconfig.json (Configured) *new* + projectStateVersion: 1 + projectProgramVersion: 1 + autoImportProviderHost: false + +ScriptInfos:: +/home/src/projects/project/src/file.ts (Open) *new* + version: SVC-1-0 + containingProjects: 1 + /home/src/projects/project/tsconfig.json *default* +/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts *new* + version: Text-1 + containingProjects: 1 + /home/src/projects/project/tsconfig.json + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "getCombinedCodeFix", + "arguments": { + "scope": { + "type": "file", + "args": { + "file": "/home/src/projects/project/src/file.ts" + } + }, + "fixId": "installTypesPackage" + }, + "seq": 2, + "type": "request" + } +TI:: Creating typing installer + +TI:: [hh:mm:ss:mss] Global cache location '/home/src/Library/Caches/typescript', safe file path '/home/src/tslibs/TS/Lib/typingSafeList.json', types map path /home/src/tslibs/TS/Lib/typesMap.json +TI:: [hh:mm:ss:mss] Processing cache location '/home/src/Library/Caches/typescript' +TI:: [hh:mm:ss:mss] Trying to find '/home/src/Library/Caches/typescript/package.json'... +TI:: [hh:mm:ss:mss] Finished processing cache location '/home/src/Library/Caches/typescript' +TI:: [hh:mm:ss:mss] Npm config file: /home/src/Library/Caches/typescript/package.json +TI:: [hh:mm:ss:mss] Npm config file: '/home/src/Library/Caches/typescript/package.json' is missing, creating new one... +TI:: [hh:mm:ss:mss] Updating types-registry npm package... +TI:: [hh:mm:ss:mss] npm install --ignore-scripts types-registry@latest +TI:: [hh:mm:ss:mss] Updated types-registry npm package +TI:: typing installer creation complete +//// [/home/src/Library/Caches/typescript/package.json] +{ "private": true } + +//// [/home/src/Library/Caches/typescript/node_modules/types-registry/index.json] +{ + "entries": { + "vscode": { + "latest": "1.3.0", + "ts2.0": "1.0.0", + "ts2.1": "1.0.0", + "ts2.2": "1.2.0", + "ts2.3": "1.3.0", + "ts2.4": "1.3.0", + "ts2.5": "1.3.0", + "ts2.6": "1.3.0", + "ts2.7": "1.3.0" + } + } +} + + +TI:: [hh:mm:ss:mss] Sending response: + { + "kind": "event::typesRegistry", + "typesRegistry": { + "vscode": { + "latest": "1.3.0", + "ts2.0": "1.0.0", + "ts2.1": "1.0.0", + "ts2.2": "1.2.0", + "ts2.3": "1.3.0", + "ts2.4": "1.3.0", + "ts2.5": "1.3.0", + "ts2.6": "1.3.0", + "ts2.7": "1.3.0" + } + } + } +Info seq [hh:mm:ss:mss] response: + { + "response": { + "changes": [], + "commands": [ + { + "type": "install package", + "file": "/home/src/projects/project/src/file.ts", + "packageName": "@types/node" + }, + { + "type": "install package", + "file": "/home/src/projects/project/src/file.ts", + "packageName": "@types/node" + }, + { + "type": "install package", + "file": "/home/src/projects/project/src/file.ts", + "packageName": "@types/vscode" + } + ] + }, + "responseRequired": true + } +After request + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "applyCodeActionCommand", + "arguments": { + "command": { + "type": "install package", + "file": "/home/src/projects/project/src/file.ts", + "packageName": "; echo 'hello' #" + } + }, + "seq": 3, + "type": "request" + } +TI:: [hh:mm:ss:mss] '; echo 'hello' #':: Package name '; echo 'hello' #' contains invalid characters +TI:: [hh:mm:ss:mss] Sending response: + { + "kind": "action::packageInstalled", + "projectName": "/home/src/projects/project/tsconfig.json", + "id": 1, + "success": false, + "message": "'; echo 'hello' #':: Package name '; echo 'hello' #' contains invalid characters" + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "setTypings", + "body": { + "kind": "action::packageInstalled", + "projectName": "/home/src/projects/project/tsconfig.json", + "id": 1, + "success": false, + "message": "'; echo 'hello' #':: Package name '; echo 'hello' #' contains invalid characters" + } + } +Info seq [hh:mm:ss:mss] response: + { + "response": {}, + "responseRequired": true + } +After request + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "applyCodeActionCommand", + "arguments": { + "command": { + "type": "install package", + "file": "/home/src/projects/project/src/file.ts", + "packageName": "react'test" + } + }, + "seq": 4, + "type": "request" + } +TI:: [hh:mm:ss:mss] 'react'test':: Package name 'react'test' contains invalid characters +TI:: [hh:mm:ss:mss] Sending response: + { + "kind": "action::packageInstalled", + "projectName": "/home/src/projects/project/tsconfig.json", + "id": 2, + "success": false, + "message": "'react'test':: Package name 'react'test' contains invalid characters" + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "setTypings", + "body": { + "kind": "action::packageInstalled", + "projectName": "/home/src/projects/project/tsconfig.json", + "id": 2, + "success": false, + "message": "'react'test':: Package name 'react'test' contains invalid characters" + } + } +Info seq [hh:mm:ss:mss] response: + { + "response": {}, + "responseRequired": true + } +After request + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "applyCodeActionCommand", + "arguments": { + "command": { + "type": "install package", + "file": "/home/src/projects/project/src/file.ts", + "packageName": "a/b/c" + } + }, + "seq": 5, + "type": "request" + } +TI:: [hh:mm:ss:mss] 'a/b/c':: Package name 'a/b/c' contains invalid characters +TI:: [hh:mm:ss:mss] Sending response: + { + "kind": "action::packageInstalled", + "projectName": "/home/src/projects/project/tsconfig.json", + "id": 3, + "success": false, + "message": "'a/b/c':: Package name 'a/b/c' contains invalid characters" + } +Info seq [hh:mm:ss:mss] event: + { + "seq": 0, + "type": "event", + "event": "setTypings", + "body": { + "kind": "action::packageInstalled", + "projectName": "/home/src/projects/project/tsconfig.json", + "id": 3, + "success": false, + "message": "'a/b/c':: Package name 'a/b/c' contains invalid characters" + } + } +Info seq [hh:mm:ss:mss] response: + { + "response": {}, + "responseRequired": true + } +After request + +Before running PendingInstalls callback:: count: 0 + +After running PendingInstalls callback:: count: 0