Skip to content

Commit 130ffde

Browse files
committed
🛡️ Sentinel: [CRITICAL] Fix command injection in LLMEnricher
1 parent 61cba58 commit 130ffde

2 files changed

Lines changed: 14 additions & 15 deletions

File tree

src/ledgermind/core/reasoning/llm_enrichment.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -288,14 +288,15 @@ def _call_cli_model(self, prompt: str) -> Optional[str]:
288288
with open(prompt_file, "w", encoding="utf-8") as f:
289289
f.write(prompt)
290290

291-
if self.client_name == "gemini":
292-
# Using shell redirect for 100% reliable capture
293-
cmd = f"gemini --model {self.model_name or 'gemini-2.5-flash-lite'} --prompt \"$(cat {prompt_file})\" > {response_file} 2>/dev/null"
294-
subprocess.run(cmd, shell=True, check=True, timeout=120) # nosec B602
295-
296-
elif self.client_name == "claude":
297-
cmd = f"claude \"$(cat {prompt_file})\" > {response_file} 2>/dev/null"
298-
subprocess.run(cmd, shell=True, check=True, timeout=120) # nosec B602
291+
with open(response_file, "w", encoding="utf-8") as f_out:
292+
with open(os.devnull, "w") as f_err:
293+
if self.client_name == "gemini":
294+
cmd = ["gemini", "--model", str(self.model_name or "gemini-2.5-flash-lite"), "--prompt", prompt]
295+
subprocess.run(cmd, stdout=f_out, stderr=f_err, check=True, timeout=120) # nosec B603
296+
297+
elif self.client_name == "claude":
298+
cmd = ["claude", prompt]
299+
subprocess.run(cmd, stdout=f_out, stderr=f_err, check=True, timeout=120) # nosec B603
299300

300301
# Read response from buffer
301302
if os.path.exists(response_file):

tests/core/reasoning/test_llm_enrichment_full.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,11 @@ def test_rich_mode_prefers_gemini_cli(mock_run, sample_proposal):
6666

6767
# Mock successful gemini run with file-writing side effect
6868
def side_effect(cmd, **kwargs):
69-
# Extract response file path from cmd string
70-
import re
71-
match = re.search(r'> (/[^ ]+)', cmd)
72-
if match:
73-
res_path = match.group(1)
74-
with open(res_path, "w", encoding="utf-8") as f:
75-
f.write("<goal>CLI Goal</goal><rationale>Human Gemini Text</rationale><compressive>CLI Comp</compressive>")
69+
# The file is passed via kwargs["stdout"] when using the subprocess.run without shell=True
70+
stdout_file = kwargs.get("stdout")
71+
if stdout_file and hasattr(stdout_file, "write"):
72+
stdout_file.write("<goal>CLI Goal</goal><rationale>Human Gemini Text</rationale><compressive>CLI Comp</compressive>")
73+
stdout_file.flush()
7674
return MagicMock(returncode=0)
7775

7876
mock_run.side_effect = side_effect

0 commit comments

Comments
 (0)