fix: don't embed shell-unsafe install paths in statusline setup nudge (#224)
The SessionStart nudge built a statusLine command by interpolating the plugin's __dirname path into a double-quoted shell string. A clone path containing shell metacharacters (quotes, &, $, backtick, ;) could break out when the suggested command later runs via the statusline shell. Low severity in practice: the path is the install location, so triggering it requires installing into a maliciously-named directory, i.e. the attacker already controls the filesystem. Hardening it anyway. Gate the snippet behind isShellSafe() (allowlist of ordinary path chars, allowing : \ / for normal Windows and POSIX paths). Unsafe paths fall back to a manual-setup instruction instead of an embeddable command. An allowlist beats a per-shell escaper, which is its own edge-case bug farm. Refs #200
This commit is contained in:
+23
-12
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { getDefaultMode, getClaudeDir } = require('./ponytail-config');
|
const { getDefaultMode, getClaudeDir, isShellSafe } = require('./ponytail-config');
|
||||||
const { getPonytailInstructions } = require('./ponytail-instructions');
|
const { getPonytailInstructions } = require('./ponytail-instructions');
|
||||||
const {
|
const {
|
||||||
clearMode,
|
clearMode,
|
||||||
@@ -57,17 +57,28 @@ if (!isCodex && !isCopilot) try {
|
|||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const scriptName = isWindows ? 'ponytail-statusline.ps1' : 'ponytail-statusline.sh';
|
const scriptName = isWindows ? 'ponytail-statusline.ps1' : 'ponytail-statusline.sh';
|
||||||
const scriptPath = path.join(__dirname, scriptName);
|
const scriptPath = path.join(__dirname, scriptName);
|
||||||
const command = isWindows
|
if (isShellSafe(scriptPath)) {
|
||||||
? `powershell -ExecutionPolicy Bypass -File "${scriptPath}"`
|
const command = isWindows
|
||||||
: `bash "${scriptPath}"`;
|
? `powershell -ExecutionPolicy Bypass -File "${scriptPath}"`
|
||||||
const statusLineSnippet =
|
: `bash "${scriptPath}"`;
|
||||||
'"statusLine": { "type": "command", "command": ' + JSON.stringify(command) + ' }';
|
const statusLineSnippet =
|
||||||
output += "\n\n" +
|
'"statusLine": { "type": "command", "command": ' + JSON.stringify(command) + ' }';
|
||||||
"STATUSLINE SETUP NEEDED: The ponytail plugin includes a statusline badge showing active mode " +
|
output += "\n\n" +
|
||||||
"(e.g. [PONYTAIL], [PONYTAIL:ULTRA]). It is not configured yet. " +
|
"STATUSLINE SETUP NEEDED: The ponytail plugin includes a statusline badge showing active mode " +
|
||||||
"To enable, add this to ~/.claude/settings.json: " +
|
"(e.g. [PONYTAIL], [PONYTAIL:ULTRA]). It is not configured yet. " +
|
||||||
statusLineSnippet + " " +
|
"To enable, add this to ~/.claude/settings.json: " +
|
||||||
"Proactively offer to set this up for the user on first interaction.";
|
statusLineSnippet + " " +
|
||||||
|
"Proactively offer to set this up for the user on first interaction.";
|
||||||
|
} else {
|
||||||
|
// ponytail: install path has shell metacharacters — don't embed it in a
|
||||||
|
// command snippet; have the agent wire it up by hand instead.
|
||||||
|
output += "\n\n" +
|
||||||
|
"STATUSLINE SETUP NEEDED: The ponytail plugin includes a statusline badge showing active mode. " +
|
||||||
|
"Its install path contains characters unsafe to embed in a shell command, so configure it manually: " +
|
||||||
|
"add a statusLine command of type \"command\" that runs " + scriptName +
|
||||||
|
" from the plugin's hooks directory to ~/.claude/settings.json, quoting/escaping the path for your shell. " +
|
||||||
|
"Proactively offer to set this up for the user on first interaction.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent fail — don't block session start over statusline detection
|
// Silent fail — don't block session start over statusline detection
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ function isDeactivationCommand(text) {
|
|||||||
return t === 'stop ponytail' || t === 'normal mode';
|
return t === 'stop ponytail' || t === 'normal mode';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ponytail: only embed the plugin install path in a statusline shell command when
|
||||||
|
// it's made of ordinary path characters. An allowlist beats escaping every shell's
|
||||||
|
// metacharacters; a hostile clone path (quotes, &, $, backtick, ;, etc.) falls back
|
||||||
|
// to manual setup instead. Allows : \ / for normal Windows and POSIX paths. Full
|
||||||
|
// per-shell escaper only if a real need appears.
|
||||||
|
function isShellSafe(p) {
|
||||||
|
return typeof p === 'string' && /^[A-Za-z0-9 _.\-:/\\~]+$/.test(p);
|
||||||
|
}
|
||||||
|
|
||||||
function getConfigDir() {
|
function getConfigDir() {
|
||||||
if (process.env.XDG_CONFIG_HOME) {
|
if (process.env.XDG_CONFIG_HOME) {
|
||||||
return path.join(process.env.XDG_CONFIG_HOME, 'ponytail');
|
return path.join(process.env.XDG_CONFIG_HOME, 'ponytail');
|
||||||
@@ -104,6 +113,7 @@ module.exports = {
|
|||||||
getConfigDir,
|
getConfigDir,
|
||||||
getConfigPath,
|
getConfigPath,
|
||||||
getClaudeDir,
|
getClaudeDir,
|
||||||
|
isShellSafe,
|
||||||
normalizeMode,
|
normalizeMode,
|
||||||
normalizeConfigMode,
|
normalizeConfigMode,
|
||||||
normalizePersistedMode,
|
normalizePersistedMode,
|
||||||
|
|||||||
@@ -8,6 +8,16 @@ const { spawnSync } = require('child_process');
|
|||||||
|
|
||||||
const root = path.join(__dirname, '..');
|
const root = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
// isShellSafe gates the statusline setup snippet (issue #200): ordinary install
|
||||||
|
// paths pass, paths carrying shell metacharacters are rejected so they never get
|
||||||
|
// embedded in a shell command.
|
||||||
|
const { isShellSafe } = require('../hooks/ponytail-config');
|
||||||
|
assert.equal(isShellSafe('C:\\Users\\x\\.claude\\plugins\\ponytail\\hooks\\ponytail-statusline.ps1'), true);
|
||||||
|
assert.equal(isShellSafe('/home/u/.claude/plugins/ponytail/hooks/ponytail-statusline.sh'), true);
|
||||||
|
assert.equal(isShellSafe('/tmp/a"&calc.exe&"/x.sh'), false);
|
||||||
|
assert.equal(isShellSafe('/tmp/$(calc)/x.sh'), false);
|
||||||
|
assert.equal(isShellSafe('/tmp/a;rm -rf/x.sh'), false);
|
||||||
|
|
||||||
function run(script, env, input = '') {
|
function run(script, env, input = '') {
|
||||||
return spawnSync(process.execPath, [path.join(root, 'hooks', script)], {
|
return spawnSync(process.execPath, [path.join(root, 'hooks', script)], {
|
||||||
env: { ...process.env, ...env },
|
env: { ...process.env, ...env },
|
||||||
|
|||||||
Reference in New Issue
Block a user