fix(security): resolve Aikido findings — command injection, path traversal, prototype pollution#77
Conversation
- Command injection (CWE-78): swap shell-based execSync("git …") for
execFileSync("git", [args]) in session, loader, and the capture-photo,
memory, and skill-learner tools.
- Path traversal (CWE-22): confine paths to their base dir in knowledge,
tool-loader (+ runtime allowlist), plugins (entry/prompt), and the
memory tool; sanitizeBranch now strips "\" and ".." too.
- Input validation: enforce kebab-case/safe names before building paths
in schedules, workflows, plugin-cli, plugins, skill-learner,
task-tracker, config, and loader.
- Prototype pollution (CWE-1321): deepMerge ignores __proto__/constructor/
prototype in config and loader.
- Deps/CI: add basic-ftp and uuid overrides; set persist-credentials:false
and pin Node 22.14.0 in publish.yml.
shreyas-lyzr
left a comment
There was a problem hiding this comment.
Solid security hardening overall — the execFileSync migrations, deepMerge prototype-pollution guards, and path-traversal checks with resolve/relative are correct and well-applied. A few issues worth addressing before merge.
Scope creep in package.json
The PR description says no user-facing feature changes, but package.json adds @open-gitagent/voice as a hard production dependency (not in the base branch) and pulls in a large transitive graph (baileys, whatsapp-rust-bridge, pino, sharp, libsignal/GPL-3.0, etc.). That is unrelated to the Aikido findings and belongs in a separate PR. If this is an accidental rebase artifact it should be reverted. If intentional, it needs its own review including license due diligence on libsignal (GPL-3.0 in a production dependency is a concern for a library codebase).
knowledge.ts: path traversal guard only covers always_load entries
The traversal check in the diff protects preloaded entries, but the available entries array (the else branch, line 56 in the file) has no guard. If the read tool later joins knowledgeDir + entry.path without its own validation, a malicious index.yaml could still escape the knowledge directory for on-demand reads. Apply the same resolve/relative check before pushing to available, or confirm that on-demand reads are independently guarded elsewhere.
sanitizeBranch: non-iterative replacement leaves trailing dot
The .replace(/../g, '') is a single pass, so the string '...' (three dots) becomes '.' — the trailing dot survives. The result does not re-introduce a traversal segment, so this is not directly exploitable as written. However an allowlist replace such as branch.replace(/[^a-zA-Z0-9_-]/g, '__') is simpler to audit, or the final historyPath join result could be validated with resolve/relative like the other guards in this PR.
Inconsistent name validation between install and load
installPlugin sanitizes the derived name with /[^a-zA-Z0-9_-]/g (allows uppercase and underscores), while discoverAndLoadPlugins validates with /^[a-zA-Z0-9_-]+$/ (also allows uppercase+underscore), and manifest validation uses KEBAB_RE (/^[a-z0-9]+(-[a-z0-9]+)*$/). A plugin whose repo name contains uppercase letters installs successfully under a name that then fails KEBAB_RE at load time. Normalize to lowercase kebab at install time and enforce a single regex everywhere.
…wledge entries, allowlist branch sanitize, normalize plugin names
shreyas-lyzr
left a comment
There was a problem hiding this comment.
All four issues from the previous review are addressed:
-
The @open-gitagent/voice dependency (and its GPL-3.0 transitive chain) is absent from this revision — clean.
-
knowledge.ts path traversal guard now covers the full entry loop before the always_load branch, so on-demand (available) entries are subject to the same resolve/relative check. The original blocking concern is resolved.
-
sanitizeBranch is now an allowlist (/[^a-zA-Z0-9_-]/g → ''), which is trivially auditable and closes the '...' → '.' edge case noted earlier.
-
installPlugin normalizes derived names to lowercase kebab-case before writing to disk, so a plugin that installs successfully will always pass KEBAB_RE at load time. The install/load mismatch is gone.
Additional hardening in this revision — execFileSync across all git ops, deepMerge prototype-pollution guards in both config.ts and loader.ts, runtime allowlist in tool-loader.ts, resolveWithinDir() for plugin entry/prompt, input validation on schedule/workflow/skill/dep names — all look correct. The allowlist approach is consistently applied and the guards are placed at the right boundary (before any path join is used for I/O).
No new issues found. Good to merge.
Summary
Addresses the security findings from the Aikido scan by hardening how
untrusted input (tool args, plugin manifests, config values, branch/skill
names) is handled across the codebase. No user-facing feature changes.
What changed
Command injection (CWE-78)
Replaced shell-interpolated
execSync("git …")withexecFileSync("git", [args])so names/paths can't break out into shellcommands.
session.ts— all git ops (clone/fetch/checkout/commit/push)loader.ts— git clone forextends/dependenciestools/capture-photo.ts,tools/memory.ts,tools/skill-learner.ts— git add/commitPath traversal (CWE-22)
Confine file access to the intended base directory.
knowledge.ts— skip entries whose path escapes the knowledge dirtool-loader.ts— reject tool scripts outsidetools/; add runtimeallowlist (
sh/bash/node/python3/python)plugins.ts—resolveWithinDir()guards pluginentry/promptpathstools/memory.ts— memory-layer path must stay within the agent dirchat-history.ts—sanitizeBranch()now strips\and..(not just/); exported and reused bycontext.tsInput validation
Require safe kebab-case/identifier names before they're used to build
filesystem paths:
schedules(id),workflows(name),plugin-cli+plugins(plugin name),skill-learner(skill_name),task-tracker(skill_used),
config(env),loader(dep name).Prototype pollution (CWE-1321)
deepMerge()ignores__proto__/constructor/prototypekeys inconfig.tsandloader.ts.Dependencies / CI hardening
package.json—overridesforbasic-ftpanduuid(patch vulnerabletransitive deps)
publish.yml—persist-credentials: falseon checkout; pin Node to22.14.0Testing
tsccompiles cleantraversal, kebab-case validation, branch sanitizing) — all blocking/
allowing as expected
sanitizeBranchchange does not affect the voice webUI (branch listing reads real git branches; filenames are unchanged for
all valid branch names)