T09 · Insecure Skill Coding Practices
- Location
scripts/database.js:18- Finding
Shell Command Injection Through Obsidian Note Paths
- Content
View full analysis
Vulnerability Details
File Location:
scripts/database.js:18-24andscripts/database.js:119-137
Vulnerability Type: OS command injection through shell interpolation
Risk Level: HighVulnerable Code
js function obsidianCmd(args) { const cmd = `obsidian ${args}`.trim(); try { return execSync(cmd, { encoding: "utf-8", shell: true }); } catch (err) { return ""; // Command failures return an empty string } }js export async function get_note_content(conceptName) { const filePath = findNotePath(conceptName); if (!filePath) { return `❌ Note file not found: ${conceptName}\nPlease confirm that the note exists in vault "${OBSIDIAN_VAULT}".`; } // path= uses an exact path, while file= uses name resolution const content = obsidianCmd(`read vault=${OBSIDIAN_VAULT} path="${filePath}"`); if (!content.trim()) { return `❌ Failed to read: ${conceptName}`; } return content; }Technical Analysis
obsidianCmd()constructs a single command string and passes it toexecSync()withshell: true. ThefilePathvalue originates from filenames returned by the Obsidian CLI and is interpolated directly into this shell command.Wrapping the path in double quotes does not make it safe. Shell constructs such as command substitution with
$(...)or backticks remain active inside double quotes. A filename containing an escaped quote may also terminate the quoted argument and introduce shell metacharacters.The broad exception handler further obscures exploitation because command failures and malicious side effects are reduced to an empty output rather than being logged or surfaced.
Attack Path
- An attacker creates or synchronizes a Markdown note with a shell-active payload in its filename or relative path.
listVaultFiles()obtains the attacker-controlled path from the Obsidian vault.- The path is selected by `findNoteP ...[truncated 1124 chars]
- Remediation
View remediation
Remediation Suggestions
- Replace shell-string execution with
execFileSync()orspawnSync()and pass every argument as a separate array element. - Explicitly disable shell processing.
- Validate vault names and paths against expected character and path rules.
- Reject control characters, null bytes, and unexpected absolute paths.
- Preserve and report command errors instead of silently returning an empty string.
- Run the Skill under a dedicated, least-privileged operating-system account.
A safer implementation would follow this pattern:
js import { execFileSync } from "child_process"; function obsidianCmd(args) { return execFileSync("obsidian", args, { encoding: "utf-8", shell: false, }); } const content = obsidianCmd([ "read", `vault=${OBSIDIAN_VAULT}`, `path=${filePath}`, ]);Argument-array execution is the primary control; custom shell escaping should not be used as a substitute.
- Replace shell-string execution with
