Back to skill

Security audit

autodl-train

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent remote training helper, but its SSH scripts can exceed the promised project-folder boundary in some configurations, so it needs review before install.

Install only after reviewing the scripts and using a dedicated non-root SSH account limited to the training project. Keep log_path and log_candidates relative to the project, set a non-empty process_match or train_command, prefer key-based SSH over password mode, and avoid using this against servers that contain unrelated sensitive workloads.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/common.py:281
Finding
Base64-Encoded Remote Shell Execution Obscures the Effective Payload<![CDATA[ ## Vulnerability Details **File Location**: `scripts/common.py:281-286` **Vulnerability Type**: Encoded shell payload execution **Risk Level**: High ### Vulnerable Code ```python if inline_script is None: command.extend([target, "bash", "-s", "--"]) else: encoded_script = base64.b64encode(inline_script.encode("utf-8")).decode("ascii") remote_command = f"printf %s {shell_quote(encoded_script)} | base64 -d | bash -s --" command.extend([target, remote_command]) ``` The encoded execution path is selected for password authentication in `scripts/common.py:307-322`: ```python ssh_password = config.get("ssh_password") command = build_ssh_command(config, inline_script=script if ssh_password else None) env = os.environ.copy() askpass_script: Optional[str] = None if ssh_password: askpass_script = _make_askpass_script() env["AUTOCLAW_TRAIN_SSH_PASSWORD"] = str(ssh_password) env["SSH_ASKPASS"] = askpass_script env["SSH_ASKPASS_REQUIRE"] = "force" env.setdefault("DISPLAY", "autoclaw:0") try: result = subprocess.run( command, input=None if ssh_password else script, text=True, capture_output=True, timeout=timeout, env=env, ) ``` ### Technical Analysis When password authentication is enabled, the generated remote shell script is Base64-encoded locally, placed directly in the SSH command line, decoded remotely, and piped into `bash`. Base64 does not provide confidentiality or integrity; it only obscures the effective shell payload from ordinary command inspection and security controls that inspect cleartext command arguments. The audit did not find an external recipient or encoded sensitive information, so the static pre-scan concern about covert data exfiltration was not confirmed. Nevertheless, decode-and-execute behavior is unnecessary for SSH password authentication and makes the actual command harder to review and constrain. The decoded script executes with all priv ...[truncated 1194 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove Base64 decode-and-execute behavior. 2. Send the shell script through SSH standard input in both key and password modes: ```python command = build_ssh_command(config, inline_script=None) result = subprocess.run( command, input=script, text=True, capture_output=True, timeout=timeout, env=env, ) ``` 3. Retain `SSH_ASKPASS` only as the authentication mechanism; it does not require the remote program to be embedded in the command line. 4. Prefer a non-root account restricted to the training project and required GPU resources. 5. Log a hash or reviewed representation of the generated script before execution. 6. Enforce server-side SSH restrictions where practical, such as a dedicated account, restricted filesystem permissions, and a forced command wrapper. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/common.py:339
Finding
Absolute Log Paths Bypass the Declared Project-Directory Boundary<![CDATA[ ## Vulnerability Details **File Location**: `scripts/common.py:339-343`, `scripts/common.py:450-467`, `scripts/remote_train.py:35-61` **Vulnerability Type**: Insufficient remote path authorization **Risk Level**: High ### Vulnerable Code The path helper accepts every absolute path without validating it against `project_path` or `allowed_project_roots`: ```python def remote_path(config: Dict[str, Any], path_value: str) -> str: if path_value.startswith("/"): return path_value return posixpath.normpath(posixpath.join(config["project_path"], path_value)) ``` The log reader subsequently reads the supplied path after only changing into the project directory: ```python def read_remote_file_tail(config: Dict[str, Any], path_value: str, *, tail_lines: int) -> Dict[str, Any]: script = f""" {build_guard_block(config)} LOG_PATH={shell_quote(path_value)} if [ ! -f "$LOG_PATH" ]; then echo "LOG_NOT_FOUND=$LOG_PATH" exit 4 fi printf 'LOG_PATH=%s\n' "$LOG_PATH" printf 'LOG_MTIME=%s\n' "$(stat -c '%y' "$LOG_PATH" 2>/dev/null || true)" printf 'LOG_SIZE=%s\n' "$(stat -c '%s' "$LOG_PATH" 2>/dev/null || echo 0)" echo '__LOG_START__' tail -n {int(tail_lines)} "$LOG_PATH" || true echo '__LOG_END__' """.strip() ``` The training launcher also accepts the resolved log path and appends data to it: ```python TRAIN_LOG={shell_quote(log_path)} TRAIN_COMMAND={shell_quote(train_command)} PROCESS_MATCH={shell_quote(process_match)} LAUNCHER_PATH={shell_quote(launcher_path)} mkdir -p "$(dirname "$TRAIN_LOG")" PRE_START_LOG_SIZE=$(stat -c '%s' "$TRAIN_LOG" 2>/dev/null || echo 0) printf '=== AutoDL train operator start ===\n' >> "$TRAIN_LOG" printf 'timestamp=%s\n' "$(date '+%F %T %z')" >> "$TRAIN_LOG" printf 'project=%s\n' "$PROJECT_PATH" >> "$TRAIN_LOG" printf 'command=%s\n' "$TRAIN_COMMAND" >> "$TRAIN_LOG" ``` ### Technical Analysis The Skill states that it operates only inside the configured `project_path`. Although `validate_required_config()` validat ...[truncated 1963 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reject absolute `log_path`, `log_candidates`, checkpoint, and launcher paths unless an explicit use case requires them. 2. Canonically resolve each path on the remote host and verify that it remains under the canonical project directory. 3. Account for symbolic links. A lexical prefix check alone is insufficient; use `realpath` after resolving the nearest existing parent. 4. Add a reusable remote guard such as: ```bash PROJECT_REAL="$(realpath -- "$PROJECT_PATH")" TARGET_REAL="$(realpath -m -- "$LOG_PATH")" case "$TARGET_REAL" in "$PROJECT_REAL"|"$PROJECT_REAL"/*) ;; *) echo "Refusing path outside project directory" >&2 exit 2 ;; esac ``` 5. Validate every configured log candidate before using `stat`, `tail`, redirection, or `mkdir`. 6. Use a dedicated unprivileged remote account whose filesystem permissions are limited to the intended project. 7. Add tests covering absolute paths, `..` traversal, symlinks escaping the project, and similarly prefixed directories. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/common.py:406
Finding
Empty Process Matcher Causes Remote Process Enumeration<![CDATA[ ## Vulnerability Details **File Location**: `scripts/common.py:406-412`, `scripts/check_status.py:25-39`, `scripts/check_status.py:121-150` **Vulnerability Type**: Overly broad process discovery and information exposure **Risk Level**: Medium ### Vulnerable Code The process matcher can be empty when neither `process_match` nor `train_command` is configured: ```python def build_process_match(config: Dict[str, Any]) -> str: explicit = (config.get("process_match") or "").strip() if explicit: return explicit command = str(config.get("train_command") or "").strip() return " ".join(command.split()[:6]) ``` The empty value is then passed to fixed-string `grep`: ```python def build_status_probe_script(config: Dict[str, Any]) -> str: process_match = build_process_match(config) return f""" {build_guard_block(config)} PROCESS_MATCH={shell_quote(process_match)} printf 'TIME=%s\n' "$(date '+%F %T %z')" printf 'PROCESS_MATCH=%s\n' "$PROCESS_MATCH" echo '__PROCESS_START__' ps -eo pid=,lstart=,etimes=,command= | grep -F -- "$PROCESS_MATCH" | grep -v 'grep -F' || true echo '__PROCESS_END__' echo '__GPU_START__' if command -v nvidia-smi >/dev/null 2>&1; then nvidia-smi --query-compute-apps=pid,process_name,used_gpu_memory --format=csv,noheader,nounits 2>/dev/null || true fi echo '__GPU_END__' """.strip() ``` Status validation does not require a training command: ```python config = load_config(args) validate_required_config(config, require_train_command=False) probe_result = run_remote_script(config, build_status_probe_script(config)) ``` ### Technical Analysis `grep -F -- ""` matches every input line. Therefore, when both process-matching fields are absent or empty, the status probe returns every process visible to the SSH user instead of only the configured training process. This condition is reachable because `check_status.py` calls configuration validation with `require_train_command=False`, and there is no separate require ...[truncated 1189 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require either a non-empty `process_match` or a valid `train_command` before running status checks. 2. Reject empty, whitespace-only, and overly broad values. 3. Add an explicit check before constructing the probe: ```python process_match = build_process_match(config).strip() if not process_match: raise SkillError("process_match or train_command is required for status checks") ``` 4. Prefer recording the launcher PID in a project-scoped PID file when training starts, then inspect only that PID. 5. Verify the PID's executable, working directory, or start time before treating it as the training process. 6. Avoid returning complete command lines by default. Return only the PID, executable name, elapsed time, and a redacted identifier unless raw output is explicitly requested. 7. Add regression tests confirming that missing matcher fields fail closed instead of enumerating processes. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (21)

Credential Access

High
Category
Privilege Escalation
Content
.env
.env.local
config.local.json
config.*.local.json
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
.env
.env.local
config.local.json
config.*.local.json
config.private.json
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
"host": "YOUR_AUTODL_HOST",
  "port": 22,
  "username": "root",
  "ssh_key_path": "~/.ssh/id_rsa",
  "strict_host_key_checking": "accept-new",
  "project_path": "/root/autodl-tmp/your-project",
  "allowed_project_roots": [
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
"host": "YOUR_AUTODL_HOST",
  "port": 22,
  "username": "root",
  "ssh_key_path": "~/.ssh/id_rsa",
  "strict_host_key_checking": "accept-new",
  "project_path": "/root/autodl-tmp/your-project",
  "allowed_project_roots": [
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
"host": "region-1.autodl.example",
  "port": 22,
  "username": "root",
  "ssh_key_path": "~/.ssh/id_ed25519",
  "project_path": "/root/autodl-tmp/your-project",
  "allowed_project_roots": ["/root/autodl-tmp"],
  "env_name": "llm-train",
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def __init__(self, description: str) -> None:
        super().__init__(description=description)
        self.add_argument("--config", help="Path to a JSON config file.")
        self.add_argument("--env-file", help="Path to a .env file.")
        self.add_argument("--host")
        self.add_argument("--port", type=int)
        self.add_argument("--username")
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
file_data = json.load(handle)
        config.update(file_data)

    env_data = os.environ.copy()
    env_data.update(load_env_file(getattr(args, "env_file", None)))
    for key, env_name in ENV_MAP.items():
        value = env_data.get(env_name)
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
file_data = json.load(handle)
        config.update(file_data)

    env_data = os.environ.copy()
    env_data.update(load_env_file(getattr(args, "env_file", None)))
    for key, env_name in ENV_MAP.items():
        value = env_data.get(env_name)
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill explicitly instructs the agent to read files, access environment-provided secrets, and execute shell commands over SSH, but it does not declare any tool scope such as permissions or allowed-tools. That creates a capability/visibility mismatch: a reviewer or enforcement layer cannot easily constrain or audit what the skill is allowed to access, increasing the risk of unintended command execution, secret exposure, or broader filesystem interaction than expected.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
- Confirm `host`, `port`, `username`, and `ssh_key_path`.
- Test SSH manually with the same host and key.
- Ensure the key has correct permissions such as `chmod 600 ~/.ssh/id_rsa`.
- If host key prompts block automation, keep `strict_host_key_checking` as `accept-new` or pre-populate `known_hosts`.
- If the server is password-based, export `AUTOCLAW_TRAIN_SSH_PASSWORD` before running the scripts.
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The code handles an SSH password by placing it into an environment variable and creating an askpass helper script, which increases exposure of a sensitive credential to local process inspection, crash dumps, inherited environments, or accidental logging. In a skill explicitly designed to operate remote servers, this is more dangerous because compromise of the credential grants direct infrastructure access.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
env.setdefault("DISPLAY", "autoclaw:0")

    try:
        result = subprocess.run(
            command,
            input=None if ssh_password else script,
            text=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
This Python file contains multiple natural-language strings presented to users in Chinese, such as failure causes and remediation guidance. The skill does not indicate that it is intentionally region-specific or provide any user opt-in for language selection, which conflicts with the language/locale policy criterion.

Natural-Language Policy Violations

Medium
Confidence
98% confidence
Finding
The convergence assessment strings returned by the code are all in Chinese, and they flow directly into the final human-readable summary. Because no alternative language path or opt-in is provided, the skill enforces a specific locale in user-visible output.

Natural-Language Policy Violations

Medium
Confidence
99% confidence
Finding
The assembled `human_summary` is entirely composed of Chinese phrases, which means end users receive output in a fixed language regardless of preference. This is a natural-language policy issue because the file does not offer opt-in, negotiation, or justification for the locale restriction.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The assessment strings returned to the user are hard-coded in Chinese, including the default summary and bottleneck warnings. This forces a specific language for user-facing output without opt-in or justification, which matches the language/locale policy violation criteria.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
{build_activation_block(config)}
exec {train_command}
EOF_AUTOCLAW_LAUNCHER
chmod 700 "$LAUNCHER_PATH"
nohup "$LAUNCHER_PATH" >> "$TRAIN_LOG" 2>&1 < /dev/null &
PID=$!
sleep 2
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Session Persistence

Medium
Category
Rogue Agent
Content
exec {train_command}
EOF_AUTOCLAW_LAUNCHER
chmod 700 "$LAUNCHER_PATH"
nohup "$LAUNCHER_PATH" >> "$TRAIN_LOG" 2>&1 < /dev/null &
PID=$!
sleep 2
RUNNING=0
Confidence
65% confidence
Finding
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.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The short description forces a specific language in user-facing metadata and does not offer any language/locale choice or explain that the skill is intended only for Chinese-speaking users. This can violate language/locale policy when skills are expected to avoid imposing a language without user opt-in.

Vague Triggers

Low
Confidence
76% confidence
Finding
This is a manifest/config file, so vague-trigger checks apply. The file defines operational parameters for SSH access and training execution, but provides no activation constraints, invocation scope, or exclusion conditions that would clarify when this skill should or should not be used.

Natural-Language Policy Violations

Low
Confidence
83% confidence
Finding
The example configuration hard-codes SSH access as the root user, normalizing privileged remote operation as the default path. In the context of a skill that executes training operations over SSH, this increases the blast radius of any command misuse, path manipulation, or downstream compromise on the remote host.

Static analysis

No suspicious patterns detected.