T09 · Insecure Skill Coding Practices
Error
- Location
- templates/update.sh:49
- Finding
- Arbitrary JavaScript Execution Through the Changelog Message<![CDATA[ ## Vulnerability Details **File Location**: `templates/update.sh`, lines 13 and 49-60 **Vulnerability Type**: User-controlled data interpolated into executable JavaScript **Risk Level**: High ### Vulnerable Code ```bash MESSAGE="${2:-无描述}" # Write CHANGELOG (non-build updates only) if [ "$BUMP_TYPE" != "build" ]; then node -e " const fs = require('fs'); let log = fs.readFileSync('$CHANGELOG', 'utf-8'); const entry = '\n## [$NEW_VERSION] $TODAY — $MESSAGE\n\n- $MESSAGE\n'; const firstEntry = log.indexOf('\n## ['); if (firstEntry !== -1) { log = log.slice(0, firstEntry) + entry + log.slice(firstEntry); } else { log += entry; } fs.writeFileSync('$CHANGELOG', log); " echo "📝 CHANGELOG 已更新" fi ``` ### Technical Analysis The second command-line argument is assigned to `MESSAGE` and then interpolated directly into JavaScript source passed to `node -e`. It is placed inside single-quoted JavaScript string literals without escaping quotes, backslashes, line terminators, or other JavaScript syntax. An attacker who can influence the update message can terminate the JavaScript string and inject additional statements. The injected code executes in Node.js with the same operating-system privileges and environment as the user running `update.sh`. The message is inserted twice, which may require a payload designed to preserve valid syntax at both insertion points, but this does not prevent exploitation. Quotes, comments, and embedded line breaks can be combined to produce valid injected JavaScript. ### Attack Path 1. The attacker supplies or persuades a user to supply a crafted second argument to `update.sh`. 2. The script stores the argument in `MESSAGE`. 3. Shell parameter expansion inserts the message into the source code passed to `node -e`. 4. The crafted content terminates the intended JavaScript string and introduces attacker-controlled statements. 5. Node.js executes those statements as the invoking user. 6. The injected code can use built-in ...[truncated 797 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Never construct executable JavaScript source by interpolating user-controlled data. 1. Pass the message as a positional process argument or through standard input. 2. Read it using `process.argv` and use it only as a data value. 3. Pass filenames and version values as arguments as well, rather than interpolating them into JavaScript. 4. Validate version numbers against a strict semantic-version expression. 5. Add tests using messages containing quotes, backslashes, newlines, and JavaScript comment characters. A safer pattern is: ```bash node - "$VERSION_FILE" "$CHANGELOG" "$NEW_VERSION" "$TODAY" "$MESSAGE" <<'NODE' const fs = require('fs'); const [, , versionFile, changelog, version, today, message] = process.argv; let log = fs.readFileSync(changelog, 'utf8'); const entry = `\n## [${version}] ${today} — ${message}\n\n- ${message}\n`; const firstEntry = log.indexOf('\n## ['); if (firstEntry !== -1) { log = log.slice(0, firstEntry) + entry + log.slice(firstEntry); } else { log += entry; } fs.writeFileSync(changelog, log); NODE ``` This keeps the message outside the JavaScript source and prevents it from changing the program’s syntax. ]]>
