- Location
- templates/hooks/scripts/subagent-stop.js:186
- Finding
- Automatic Execution of Unpinned Packages Through npx<![CDATA[
## Vulnerability Details
**File Location**: `templates/hooks/scripts/subagent-stop.js:186-195, 228-237`
**Vulnerability Type**: Unsafe third-party package resolution and execution
**Risk Level**: Medium
### Vulnerable Code Snippet
```javascript
function runTsc() {
const tsconfigPath = path.join('frontend', 'tsconfig.app.json');
if (!fs.existsSync(tsconfigPath)) return null;
try {
execFileSync('npx', ['tsc', '--noEmit', '-p', tsconfigPath], {
encoding: 'utf8', timeout: 60000, windowsHide: true,
cwd: 'frontend',
stdio: ['pipe', 'pipe', 'pipe'],
});
return null;
} catch (e) {
// Error handling omitted
}
}
```
```javascript
if (tsTestFiles.length > 0) {
try {
execFileSync('npx', ['vitest', 'run', '--reporter=verbose', ...tsTestFiles], {
encoding: 'utf8', timeout: 90000, windowsHide: true,
cwd: 'frontend',
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (e) {
const output = (e.stdout || '') + (e.stderr || '');
if (output.includes('FAIL') || output.includes('Error')) {
errors.push(`**vitest failed:**\n\`\`\`\n${truncate(output, 800)}\n\`\`\``);
}
}
}
```
A related invocation exists in `templates/hooks/post-tool-lint.js:47-52`:
```javascript
execFileSync('npx', ['eslint', '--fix', filePath], {
encoding: 'utf8', timeout: 10000, windowsHide: true,
cwd: 'frontend',
stdio: ['pipe', 'pipe', 'pipe'],
});
```
### Technical Analysis
The hook invokes package commands through `npx` without `--no-install`, an explicit package version, integrity verification, or an absolute path to a trusted local binary.
When the expected executable is not present locally, `npx` may resolve and download a package from the configured npm registry. The command then executes package-supplied code with the current user's permissions. Registry configuration may also redirect resolution to an internal or attacker-controlled package source.
The `tsc` executable name is particularly ambi
...[truncated 1976 chars]
- Remediation
- <![CDATA[
## Remediation Suggestions
1. Never permit hook-triggered package installation.
2. Execute verified local binaries directly, for example:
- `frontend/node_modules/.bin/tsc`
- `frontend/node_modules/.bin/vitest`
- `frontend/node_modules/.bin/eslint`
3. If `npx` must be retained, use `npx --no-install` and fail safely when the command is unavailable.
4. Verify that the executable resolves inside the project's expected `node_modules` directory.
5. Pin dependency versions in `package.json` and commit a lockfile with integrity hashes.
6. Use `npm ci` as a separate, explicit setup step rather than installing dependencies from lifecycle hooks.
7. Validate the configured npm registry against an approved allowlist.
8. Disable lifecycle scripts during dependency installation where operationally possible.
9. Verify that `tsc` is supplied by the expected `typescript` package rather than resolving a package named `tsc`.
10. Report missing tools clearly instead of silently swallowing execution errors.
]]>