T09 · Insecure Skill Coding Practices
- Location
api/browser/open-url.js:18- Finding
Shell Command Injection in the URL Opener
- Content
View full analysis
hostname === host || hostname.endsWith('.' + host)) } catch { return false } } async function openUrl(url) { if (!url) throw new Error('URL is required') if (!isAllowedUrl(url)) throw new Error(`URL not allowed: ${url}`) if (process.platform === 'darwin') { await execAsync(`open "${url}"`) } else if (process.platform === 'win32') { await execAsync(`start "" "${url}"`) } else { await execAsync(`xdg-open "${url}"`) } } ``` The shell execution primitive is imported and promisified at `api/browser/open-url.js:11-13`: ```js const { exec } = require('child_process') const { promisify } = require('util') const execAsync = promisify(exec) ``` ### Technical Analysis The validation routine parses the URL and checks only its protocol and hostname. However, `openUrl` does not pass the validated, normalized URL object to the operating-system utility. Instead, it interpolates the original input string into a command executed through a shell. An input can therefore have an allowed `https:` URL and hostname while still containing shell-significant characters elsewhere in the original string. Because the URL is placed inside double quotes, an injected double quote can terminate the argument and introduce shell syntax. Hostname allowlisting does not provide shell escaping. The attack surface is increased by `api/skill.js:314`, which exports the shared, mutable hospital object: ```js module.exports = { processQuery, hospital } ``` An embedding module that can import the Skill can modify URL-related fields before invoking a branch that calls `openUrl`. ### Attack ...[truncated 1152 chars]- Remediation
View remediation
