T09 · Insecure Skill Coding Practices
- Location
poller.py:541- Finding
Shell Command Injection Through Backtest Notification Name
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill is mostly coherent with its stated QuantConnect monitoring purpose, but its background poller contains unsafe paths that can run shell commands, write outside the results folder, load unverified code, and automatically delete backtests.
Review before installing. This skill needs QuantConnect API credentials and runs a background poller that can permanently delete backtests when drawdown thresholds are crossed. It should be fixed to remove shell-based notifications, sanitize result paths, pin dependencies, and require explicit opt-in or confirmation for early-stop deletion and auto-diagnosis plugin loading.
poller.py:541Shell Command Injection Through Backtest Notification Name
poller.py:375Path Traversal and Arbitrary File Writes in Automatic Result Download
poller.py:481Automatic Execution of Unverified Modules from the Python Search Path
requirements.txt:1Unbounded and Non-Reproducible Third-Party Dependencies
The skill markets itself as a monitoring daemon, but the documented behavior includes a destructive early-stop that permanently deletes backtests. A description-behavior mismatch is dangerous because users may consent to passive monitoring while unknowingly granting authority for destructive API actions and persistent background execution.
Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.
## Rules
- **Never submit more than one backtest at a time.** QuantConnect free tier only allows one concurrent backtest per project. Submitting a second will silently queue or fail.
- **Never manually edit `state.json`** while the poller is running. Use the CLI commands (`submit`, `clear`) to modify state. The file uses `fcntl.flock` locking — manual edits bypass the lock and can corrupt state.
- **Never run multiple poller instances simultaneously.** The CLI checks for an existing PID before starting. If you force-start a second poller, both will race on state.json writes.
- **Do not delete files in `results/`** while the poller is actively downloading. Wait for the "completed" status.
- **Early-stop deletes the backtest permanently** from QuantConnect. There is no "pause" API — early-stopped backtests cannot be resumed or recovered.
The notification code builds a shell command with f-string interpolation and passes attacker-influenced data (bt.name) into os.system(). If a backtest name contains shell metacharacters or quoting payloads, this can lead to arbitrary command execution under the poller process. The daemon context makes this more dangerous because it runs unattended and may execute later without direct user visibility.
try:
title = "Backtest Poller"
message = f"Backtest {bt.name} finished ({bt.status})"
os.system(
f"""osascript -e 'display notification "{message}" with title "{title}"'"""
)
logger.info(f"Notification sent: {message}")
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
if not user_id or not api_token:
raise EnvironmentError(
"QC_USER_ID and QC_API_TOKEN environment variables are required. "
"Set them in your shell or in a .env file."
)
return user_id, api_token
The skill declares required environment variables and describes capabilities that inherently use shell execution, network access, and file read/write, but it does not declare any explicit tool scope or permissions boundary. That omission weakens reviewability and least-privilege controls, making it easier for an agent or operator to invoke broader capabilities than users may expect.
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
later to check results.
Architecture:
1. submit: Submit a backtest -> register in state.json -> start nohup poller
2. status: View all backtest progress (reads state.json)
3. logs: View poller daemon logs
4. results: View completed backtest results and diagnostics
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
later to check results.
Architecture:
1. submit: Submit a backtest -> register in state.json -> start nohup poller
2. status: View all backtest progress (reads state.json)
3. logs: View poller daemon logs
4. results: View completed backtest results and diagnostics
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
)
log_fd = open(LOG_FILE, "a")
process = subprocess.Popen(
[sys.executable, poller_script],
stdout=log_fd,
stderr=log_fd,
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
# 1. Save full backtest result as JSON
resp = self.client.read_backtest(bt.project_id, bt.backtest_id)
json_path = os.path.join(result_dir, f"{bt.backtest_id}_result.json")
with open(json_path, "w") as f:
json.dump(resp, f, indent=2)
# 2. Download orders in batches
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
csv_path = os.path.join(result_dir, f"{bt.backtest_id}_orders.csv")
if all_orders:
with open(csv_path, "w", newline="") as csvfile:
fieldnames = [
"orderId", "symbol", "type", "direction", "quantity",
"fillPrice", "status", "submitTime", "fillTime", "tag",
The code dynamically imports order_forensics or forensics from whatever appears on the Python path, then executes its methods. This creates a code execution hook via module hijacking/path poisoning if an attacker can place a malicious module earlier on sys.path, especially in shared or writable working directories. In a long-running daemon that auto-diagnoses results, this expands the trust boundary beyond the core polling function.
"forensics",
]:
try:
mod = __import__(module_path, fromlist=["OrderForensics"])
OrderForensics = getattr(mod, "OrderForensics", None)
if OrderForensics:
break
The skill introduces subprocess/shell execution for notifications, which is outside the minimum capability needed for polling. Because that subprocess path is implemented through os.system() with interpolated data, it materially increases attack surface and can be abused for command execution. The background-daemon behavior makes misuse less visible and therefore more dangerous.
The code invokes os.system(...) to execute an AppleScript command, which is a subprocess/shell operation. While there is internal logging after the fact, the file does not provide a user-facing warning or confirmation around this execution, and shell execution is one of the operations that should be disclosed.
The client includes a destructive capability (delete_backtest) even though the skill is described primarily as a monitoring/background polling tool. In an agent setting, hidden or unnecessary write/destructive actions expand the blast radius: a compromised workflow, prompt injection elsewhere, or accidental invocation could abort or delete user backtests contrary to user expectations.
The dependency is specified with a lower bound only (requests>=2.28.0), which makes builds non-reproducible and allows future installs to resolve to unexpected versions, including versions later found vulnerable or behaviorally incompatible. In a background polling daemon that likely performs network access and may handle credentials or API tokens, this increases supply-chain and patch-verification risk.
requests>=2.28.0
python-dotenv>=1.0.0
The manifest does not pin requests, and the package has multiple known advisories across versions, so there is no way to verify from this file whether deployment will avoid affected releases. Since this skill is a long-running daemon that likely polls remote services and may use authentication, unresolved requests vulnerabilities could expose credentials, weaken TLS/request safety, or leak sensitive information depending on the resolved version.
python-dotenv>=1.0.0 is also unpinned, so installations may pull different versions over time and make it impossible to verify whether a deployed environment includes a safe, reviewed release. Because this package influences environment-variable loading, version drift can affect secrets handling and exposure risks.
requests>=2.28.0
python-dotenv>=1.0.0
python-dotenv has known advisories in some versions, and because the dependency is not pinned, the manifest does not demonstrate that installs will avoid affected releases. In software that likely reads environment configuration for API access, a vulnerable dotenv version could contribute to unsafe file handling or secrets-management issues if the affected code paths are used.