#3640·graphify

aider/devin skills still build save-result/add commands from unescaped free text (RCE via $(...) in double quotes)

Author: ayushcodes10Created Sep 17, 2026Updated Sep 17, 2026

Summary

PR #3439/#3442 fixed the disk-write-capable skill platforms so graphify save-result/graphify add build their arguments from a file instead of substituting free text (a question, an LLM-generated answer, a scraped page's author/contributor name) directly into a shell command or Python source string. The aider and devin monolith skills, which have no separate file-write tool and instead run everything through $(cat graphify-out/.graphify_python) -c "..." / inline shell commands, were not covered by that fix and still use the vulnerable pattern.

Evidence (current v8)

tools/skillgen/fragments/core/aider.md (and the byte-identical pattern in tools/skillgen/fragments/core/devin.md, lines shifted):

  • L1045, L1120, L1186 — three separate save-result invocations built as a single shell command with the free-text answer substituted directly into a double-quoted argument:

    $(cat graphify-out/.graphify_python) -m graphify save-result --question "QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2
    

    Bash still performs $(...) and backtick command substitution inside double quotes. If ANSWER (an LLM-generated answer, which can echo content from an untrusted source such as a fetched webpage) is substituted verbatim and contains $(some command), that command executes when the agent runs the line. This is not a corruption risk like single-quote breakout, it is a direct RCE vector, and it defeats the exact protection --answer-file/--question-file/--nodes-file (already shipped for disk-capable platforms in #3439) exists to provide.

  • L1202 (aider.md) / L1334 (devin.md) — the /graphify add flow builds a Python source string with the URL/author/contributor substituted into single-quoted Python literals:

    out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR')
    

    A value containing an embedded ' (or \) breaks out of the string literal, letting an attacker-controlled URL, author, or contributor field (again, free text that can originate from a fetched page) inject arbitrary Python that then runs inside the -c script — the exact "building generated code from unsanitized free text" class of bug #3439 targeted, just via a Python string literal instead of a JSON payload.

Why this needs its own fix, not a quick patch

Closing this properly needs a mechanism that works for a platform with only raw shell/python execution (no dedicated file-write tool), across all six affected call sites (3 save-result blocks x 2 files, plus the ingest() call x 2 files). The shape that avoids re-introducing the same risk: a quoted heredoc (<<'EOF' ... EOF, whose body bash does not expand at all) to get the free text onto disk without any shell interpretation, then dispatch through the already-existing --question-file/--answer-file/--nodes-file flags for save-result, and a JSON payload file (mirroring the disk-platform add fix) read via stdin or a temp file for ingest(). That is a real design/rewrite across two ~1300+ line monolith fragments plus their generated expected/ artifacts and monolith-roundtrip predicates, not a one-line fix, so it deserves its own dedicated PR with careful review rather than being folded into #3442's narrower scope.

Suggested fix direction

  1. For save-result: replace each inline --question "QUESTION" --answer "ANSWER" shell line with instructions to write QUESTION/ANSWER/NODES to temp files via a quoted heredoc (cat > "$f" <<'GRAPHIFY_EOF' ... GRAPHIFY_EOF, single-quoted delimiter so bash performs zero expansion on the body), then invoke save-result --question-file "$qf" --answer-file "$af" --nodes-file "$nf".
  2. For /graphify add: same heredoc technique to write a JSON payload file, then have the -c script read and json.load() it (mirroring the already-fixed disk-platform add-watch.md flow) instead of embedding values as Python literals.

Filed as a standalone issue per triage of PR #3442's review — the fix there addressed only the two comments actually flagged on that PR (cli.py file-read error handling, and the same-class doc wording gap in the disk-platform add-watch.md), not this pre-existing, broader gap discovered while investigating it.