T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/digest.js:269
- Finding
- Shell Command Injection in Discord Delivery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/digest.js:269-279`; equivalent vulnerable construction at `scripts/alert.js:124-134` **Vulnerability Type**: OS command injection through shell interpolation **Risk Level**: High ### Vulnerable Code ```javascript function deliverDiscord(config, content) { if (!config.delivery.discord.enabled) { console.error('❌ Discord 전송이 비활성화되어 있습니다.'); return; } const channelId = config.delivery.discord.channelId; try { execSync(`openclaw message send --channel discord --target "${channelId}" --message "${content.replace(/"/g, '\\"')}"`, { stdio: 'inherit' }); console.log('✅ Discord 전송 완료'); } catch (err) { console.error('❌ Discord 전송 실패:', err.message); } } ``` The same pattern appears in `scripts/alert.js`: ```javascript execSync(`openclaw message send --channel discord --target "${channelId}" --message "${message.replace(/"/g, '\\"')}"`, { stdio: 'inherit' }); ``` ### Technical Analysis `execSync()` receives a single command string, so Node.js invokes a shell to interpret it. Both `channelId` and message content are interpolated into that string. Escaping only double quotation marks does not prevent shell evaluation. Command substitutions such as `$(command)` and backticks are still evaluated inside double-quoted shell arguments. Backslashes and other shell syntax can also alter parsing. The channel identifier comes from the writable configuration file. In `digest.js`, message content is derived partly from stored sales JSON, including source names. Consequently, either configuration tampering or maliciously crafted report data can reach the shell command. ### Attack Path 1. An attacker obtains write access to the Skill configuration or a sales JSON file. 2. The attacker places shell syntax such as `$(malicious-command)` in `delivery.discord.channelId` or a report field included in the digest. 3. A user or scheduled job runs the digest or alert with Discor ...[truncated 813 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions - Never construct a shell command by concatenating configuration or report data. - Use an argument-array API with shell processing disabled: ```javascript const { execFileSync } = require('child_process'); execFileSync( 'openclaw', [ 'message', 'send', '--channel', 'discord', '--target', channelId, '--message', content ], { stdio: 'inherit', shell: false } ); ``` - Apply the same correction to `scripts/alert.js`. - Validate Discord channel IDs against the expected numeric format, for example `/^\d{17,20}$/`. - Validate the structure and types of loaded sales JSON before formatting it. - Run scheduled jobs under a dedicated, minimally privileged account. - Add regression tests containing `$()`, backticks, quotes, backslashes, newlines, and shell metacharacters. ]]>
