Back to skill

Security audit

SMILES-to-Docking Virtual Screening

Security checks for vulnerabilities and agentic risk

Overview

This looks like a real molecular-docking workflow, but crafted ligand names or paths could trigger shell command execution or write files outside the intended output folder.

Install only in an isolated environment and run only on trusted ligand/input filenames and trusted output paths. Avoid shared or sensitive directories, and treat SMILES files and docking-result directories from other people as untrusted until the shell=True calls and filename validation are fixed.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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 (5)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/prepare_ligand.py:30
Finding

Shell Command Injection in Ligand Conversion Fallback

Content
View full analysis
bool: """Convert SDF ligand to PDBQT using Meeko with Open Babel fallback.""" try: mol = Chem.SDMolSupplier(str(sdf_path), removeHs=False)[0] if mol is None: print(f" WARNING: Could not read SDF: {sdf_path}") return False preparator = MoleculePreparation() setup_list = preparator.prepare(mol) preparator.write_pdbqt_file(out_pdbqt, setup_list) return True except Exception as e: print(f" WARNING Meeko failed for {Path(sdf_path).name}, fallback to Open Babel: {e}") cmd = f'obabel "{sdf_path}" -O "{out_pdbqt}" --partialcharge gasteiger -h' result = subprocess.run(cmd, shell=True, capture_output=True) return result.returncode == 0 ``` The same unsafe implementation is reproduced in the source listing embedded in `SKILL.md`. ### Technical Analysis The fallback constructs a shell command by interpolating `sdf_path` and `out_pdbqt` into a string and executes it with `shell=True`. Although the values are surrounded by double quotes, embedded quote characters and shell metacharacters are not escaped. Consequently, quoting can be terminated and additional shell syntax introduced. Both values can be influenced by users: - `sdf_path` comes from files under the caller-selected `--sdf_dir`. - `out_pdbqt` is constructed under the caller-selected `--output_dir`. - In the full workflow, SDF filenames can originate from unsanitized ligand names. The vulnerable branch is reached whenever Meeko raises an exception during parsing, preparation, or writing. ### Attack Path 1. An attacker supplies a directory containing an `.sdf` file whose filename includes shell-significant characters, or infl ...[truncated 869 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/prepare_protein.py:98
Finding

Shell Command Injection in Receptor Preparation

Content
View full analysis
str: """Remove waters and non-standard residues, then convert to PDBQT with Open Babel.""" out_dir = Path(output_dir) out_dir.mkdir(parents=True, exist_ok=True) parser = PDBParser(QUIET=True) structure = parser.get_structure("protein", pdb_path) temp_pdb = out_dir / "protein_no_water.pdb" io = PDBIO() io.set_structure(structure) io.save(str(temp_pdb), NonWaterStandardSelect()) out_pdbqt = out_dir / "protein_prepared.pdbqt" cmd = f'obabel "{temp_pdb}" -O "{out_pdbqt}" -xr -h --partialcharge gasteiger' result = subprocess.run(cmd, shell=True, capture_output=True, text=True) if temp_pdb.exists(): os.remove(temp_pdb) if result.returncode != 0: raise RuntimeError(f"Open Babel failed:\n{result.stderr}") print(f" Protein prepared: {out_pdbqt}") return str(out_pdbqt) ``` The same unsafe implementation is reproduced in `SKILL.md`. ### Technical Analysis The caller-controlled `output_dir` is used to create both `temp_pdb` and `out_pdbqt`. These paths are interpolated into a command string executed with `shell=True`. Double-quoting does not neutralize an embedded quote in a directory component, allowing the shell command structure to be modified. The PDB is parsed before the vulnerable subprocess executes, so exploitation requires a valid or sufficiently parseable input PDB. No privilege escalation is necessary because the command runs directly as the workflow user. ### Attack Path 1. The attacker invokes the receptor-preparation script or full workflow with a crafted `--output_dir`. 2. The supplied protein file is successfully parsed and the temporary PDB is written. 3. The crafted directory name becomes part ...[truncated 619 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/rank_results.py:57
Finding

Shell Command Injection During Top-N Complex Export

Content
View full analysis
bool: """Merge protein and ligand PDBs into a complex using Open Babel.""" cmd = f'obabel "{protein_pdb}" "{ligand_pdb}" -O "{out_complex}" --sort' result = subprocess.run(cmd, shell=True, capture_output=True) return result.returncode == 0 ``` The same unsafe implementation is included in `SKILL.md`. ### Technical Analysis All three path values are interpolated into a shell command: - `protein_pdb` is supplied through `--protein_pdb`. - `ligand_pdb` is derived from docking-result directories and files. - `out_complex` includes the caller-selected output directory and a ligand-derived name. A quote in any path can terminate the intended quoted argument and inject shell syntax. The export function calls this helper after locating a parseable Vina log and a docked PDBQT model. ### Attack Path 1. An attacker controls the protein path, ranking output path, or a ligand directory name inside the supplied docking-results directory. 2. The docking directory contains a `vina_log.txt` with a parseable result and a PDBQT file with at least one `MODEL` record. 3. The ligand is selected within the requested Top-N results. 4. The crafted path reaches `merge_complex_obabel`. 5. The path is interpolated into `cmd` and executed with `shell=True`. 6. The system shell executes the injected command under the workflow user's account. ### Impact Assessment An attacker can execute arbitrary commands with the privileges of the user performing ranking. This can compromise local files and docking artifacts, tamper with ranked results, run additional programs, or expose user-accessible data. The vulnerability does not inherently cross operating-system privilege boundarie ...[truncated 8 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/smiles_to_sdf.py:29
Finding

Unsanitized Ligand Names Allow Path Traversal and Arbitrary File Placement

Content
View full analysis
1 else f"lig_{lineno:04d}" entries.append((smiles, name)) ``` The unvalidated name is subsequently used as a path component: ```python for i, (smiles, name) in enumerate(entries, 1): sdf_path = ligands_dir / f"{name}.sdf" if sdf_path.exists(): print(f" SKIP [{i}/{len(entries)}] {name} already exists") ``` When molecule generation succeeds, `SDWriter(str(sdf_path))` writes to that path. ### Technical Analysis The ligand name is accepted verbatim from the tab-separated input file. There is no rejection of: - Absolute paths - `..` traversal components - Directory separators - Quote characters - Shell metacharacters - Control characters - Duplicate or reserved names With `pathlib`, joining a base path to an absolute child discards the base path. Relative traversal components can also escape `ligands_dir`. As a result, generated SDF data can be placed outside the intended workflow directory. Hostile names may subsequently become filenames consumed by the shell-injectable ligand fallback. ### Attack Path 1. An attacker supplies a SMILES input containing a valid SMILES value and a ligand name with an absolute path or traversal components. 2. The SMILES is successfully converted into a molecule. 3. The untrusted name is appended with `.sdf` and combined with `ligands_dir`. 4. Path resolution targets a location outside the intended ligand directory. 5. `SDWriter` creates or overwrites the targeted `.sdf` file if the workflow user has permission. 6. If the hostile filename is later processed by `prepare_ligand.py`, shell-significant characters can also contribute to command injection when the Meeko fallback run ...[truncated 459 chars]
Remediation
View remediation
str: candidate = re.sub(r"[^A-Za-z0-9_.-]+", "_", name).strip("._") return candidate or f"lig_{lineno:04d}" ``` Before writing: 1. Reject absolute names, path separators, `..`, control characters, and reserved names. 2. Resolve the candidate output path: ```python target = (ligands_dir / f"{safe_name}.sdf").resolve() root = ligands_dir.resolve() if target.parent != root: raise ValueError("Invalid ligand name") ``` 3. Detect duplicate sanitized names and assign stable unique identifiers. 4. Keep the original ligand name only inside SDF metadata or a mapping file. 5. Apply equivalent validation wherever ligand or docking directory names are consumed. ]]>

T08 · Insecure Dependencies

Note
Location
SKILL.md:30
Finding

Unpinned Third-Party Dependencies Create Supply-Chain and Reproducibility Risk

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (19)

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
98% confidence
Finding

The code builds a shell command with user-influenced file paths and executes it with shell=True. Although paths are wrapped in double quotes, shell metacharacters such as embedded quotes or command substitutions can still break out of quoting and trigger arbitrary command execution if an attacker controls filenames or paths.

Content

Scanner excerpt · SKILL.md (reported line 264)May include surrounding context.

md
except Exception as e:
        print(f"Meeko failed for {Path(sdf_path).name}, fallback to Open Babel: {e}")
        cmd = f'obabel "{sdf_path}" -O "{out_pdbqt}" --partialcharge gasteiger -h'
        result = subprocess.run(cmd, shell=True, capture_output=True)
        return result.returncode == 0

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
98% confidence
Finding

This function constructs an Open Babel command string from filesystem paths and runs it through the shell. If a supplied protein path or derived temporary/output path contains shell-special content, an attacker may achieve command injection and execute arbitrary OS commands with the privileges of the user running the workflow.

Content

Scanner excerpt · SKILL.md (reported line 406)May include surrounding context.

md
out_pdbqt = out_dir / "protein_prepared.pdbqt"
    cmd = f'obabel "{temp_pdb}" -O "{out_pdbqt}" -xr -h --partialcharge gasteiger'
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)

    if temp_pdb.exists():
        os.remove(temp_pdb)

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
98% confidence
Finding

The merge helper interpolates protein, ligand, and output paths into a shell command and executes it with shell=True. Because these values can be influenced by upstream filenames and user arguments, a crafted path containing shell syntax could escape quoting and run arbitrary commands during result export.

Content

Scanner excerpt · SKILL.md (reported line 685)May include surrounding context.

md
def merge_complex_obabel(protein_pdb: str, ligand_pdb: str, out_complex: str) -> bool:
    cmd = f'obabel "{protein_pdb}" "{ligand_pdb}" -O "{out_complex}" --sort'
    result = subprocess.run(cmd, shell=True, capture_output=True)
    return result.returncode == 0

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
98% confidence
Finding

Using subprocess.run with shell=True on a command string derived from filesystem inputs is a real parameter-abuse issue because untrusted filenames can alter how the shell interprets the command. In this batch-processing context, ligand files may originate from external datasets, which makes the fallback conversion path more dangerous if an attacker can introduce crafted filenames.

Content

Scanner excerpt · scripts/prepare_ligand.py (reported line 31)May include surrounding context.

python
except Exception as e:
        print(f"  WARNING Meeko failed for {Path(sdf_path).name}, fallback to Open Babel: {e}")
        cmd = f'obabel "{sdf_path}" -O "{out_pdbqt}" --partialcharge gasteiger -h'
        result = subprocess.run(cmd, shell=True, capture_output=True)
        return result.returncode == 0

Self-Modification

High
Category
Rogue Agent
Confidence
85% confidence
Finding

Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Content

Scanner excerpt · scripts/prepare_ligand.py (reported line 41)May include surrounding context.

python
)
    parser.add_argument("--sdf_dir", required=True, help="Input SDF directory")
    parser.add_argument("--output_dir", required=True, help="Output PDBQT directory")
    parser.add_argument("--force", action="store_true", help="Overwrite existing files")
    args = parser.parse_args()

    sdf_dir = Path(args.sdf_dir)

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
98% confidence
Finding

This is a true tool-parameter abuse issue because the code forwards attacker-influenced path values into a shell command that invokes Open Babel. In an agent or automation context where file names or output directories may come from upstream tasks or user input, this materially increases the risk of arbitrary command execution rather than mere misuse of the external tool.

Content

Scanner excerpt · scripts/prepare_protein.py (reported line 99)May include surrounding context.

python
out_pdbqt = out_dir / "protein_prepared.pdbqt"
    cmd = f'obabel "{temp_pdb}" -O "{out_pdbqt}" -xr -h --partialcharge gasteiger'
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    if temp_pdb.exists():
        os.remove(temp_pdb)

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
99% confidence
Finding

This is a true tool-parameter-abuse issue because attacker-influenced values are passed into an Open Babel invocation through the shell. Although the paths are wrapped in double quotes, shell expansion still occurs inside double quotes for constructs like $(...), so crafted filenames or CLI-supplied paths can trigger arbitrary command execution.

Content

Scanner excerpt · scripts/rank_results.py (reported line 58)May include surrounding context.

python
def merge_complex_obabel(protein_pdb: str, ligand_pdb: str, out_complex: str) -> bool:
    """Merge protein and ligand PDBs into a complex using Open Babel."""
    cmd = f'obabel "{protein_pdb}" "{ligand_pdb}" -O "{out_complex}" --sort'
    result = subprocess.run(cmd, shell=True, capture_output=True)
    return result.returncode == 0

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
90% confidence
Finding

The script executes a user-supplied vina_path after tokenizing it with shlex.split, allowing the caller to append arbitrary extra arguments when the script probes --version. For example, a crafted vina_path containing additional flags can alter what binary is run or how it behaves, and because executable trust is based on successful execution, an attacker who controls that path can cause execution of an unintended program.

Content

Scanner excerpt · scripts/batch_docking.py (reported line 36)May include surrounding context.

python
]
    for candidate in candidates:
        if candidate and (os.path.isfile(candidate) or candidate == "vina"):
            result = subprocess.run(
                shlex.split(f"{candidate} --version") if candidate != "vina" else ["vina", "--version"],
                capture_output=True, text=True,
            )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/batch_docking.py (reported line 43)May include surrounding context.

python
if result.returncode == 0:
                print(f"  Found Vina: {candidate}")
                return candidate
    result = subprocess.run(["which", "vina"], capture_output=True, text=True)
    return result.stdout.strip() if result.returncode == 0 else ""

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/batch_docking.py (reported line 94)May include surrounding context.

python
log_path = ligand_out_dir / "vina_log.txt"
    try:
        result = subprocess.run(
            cmd, capture_output=True, text=True,
            cwd=str(ligand_out_dir), timeout=600,
        )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/main.py (reported line 27)May include surrounding context.

python
def step1_smiles_to_sdf(smiles_file: str, output_dir: str) -> str:
    script = SCRIPT_DIR / "smiles_to_sdf.py"
    subprocess.run([sys.executable, str(script),
                    "--smiles_file", smiles_file,
                    "--output_dir", output_dir], check=True)
    return output_dir

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/main.py (reported line 36)May include surrounding context.

python
def step2_prepare(sdf_dir: str, protein_pdb: str, output_dir: str) -> tuple[str, str]:
    lig_script = SCRIPT_DIR / "prepare_ligand.py"
    lig_out = os.path.join(output_dir, "ligands")
    subprocess.run([sys.executable, str(lig_script),
                    "--sdf_dir", sdf_dir,
                    "--output_dir", lig_out], check=True)

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/main.py (reported line 41)May include surrounding context.

python
"--output_dir", lig_out], check=True)

    prot_script = SCRIPT_DIR / "prepare_protein.py"
    subprocess.run([sys.executable, str(prot_script),
                    "--protein", protein_pdb,
                    "--output_dir", output_dir], check=True)

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/main.py (reported line 66)May include surrounding context.

python
]
    if vina_path:
        cmd += ["--vina_path", vina_path]
    subprocess.run(cmd, check=True)
    return output_dir

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/main.py (reported line 72)May include surrounding context.

python
def step4_rank(docking_dir: str, protein_pdb: str, top_n: int, output_dir: str) -> str:
    script = SCRIPT_DIR / "rank_results.py"
    subprocess.run([sys.executable, str(script),
                    "--docking_dir", docking_dir,
                    "--protein_pdb", protein_pdb,
                    "--top_n", str(top_n),

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
97% confidence
Finding

The fallback path builds a shell command by interpolating file paths into a string and executes it with shell=True. If an attacker can control filenames or output paths, shell metacharacters inside those values can trigger command injection despite the surrounding quotes, especially because shell parsing still honors constructs like embedded quotes or command substitutions.

Content

Scanner excerpt · scripts/prepare_ligand.py (reported line 31)May include surrounding context.

python
except Exception as e:
        print(f"  WARNING Meeko failed for {Path(sdf_path).name}, fallback to Open Babel: {e}")
        cmd = f'obabel "{sdf_path}" -O "{out_pdbqt}" --partialcharge gasteiger -h'
        result = subprocess.run(cmd, shell=True, capture_output=True)
        return result.returncode == 0

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
97% confidence
Finding

The script constructs a shell command from file paths and executes it with shell=True, which enables shell interpretation of special characters embedded in user-controlled inputs such as --output_dir or derived file names. An attacker who can supply crafted arguments could achieve command injection and run arbitrary OS commands with the privileges of the script.

Content

Scanner excerpt · scripts/prepare_protein.py (reported line 99)May include surrounding context.

python
out_pdbqt = out_dir / "protein_prepared.pdbqt"
    cmd = f'obabel "{temp_pdb}" -O "{out_pdbqt}" -xr -h --partialcharge gasteiger'
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    if temp_pdb.exists():
        os.remove(temp_pdb)

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
98% confidence
Finding

The code builds a shell command by interpolating untrusted file path inputs into a single string and executes it with shell=True. An attacker who controls --protein_pdb, ligand-derived filenames, or output paths could inject shell metacharacters and execute arbitrary OS commands, leading to full command execution in the context of the script.

Content

Scanner excerpt · scripts/rank_results.py (reported line 58)May include surrounding context.

python
def merge_complex_obabel(protein_pdb: str, ligand_pdb: str, out_complex: str) -> bool:
    """Merge protein and ligand PDBs into a complex using Open Babel."""
    cmd = f'obabel "{protein_pdb}" "{ligand_pdb}" -O "{out_complex}" --sort'
    result = subprocess.run(cmd, shell=True, capture_output=True)
    return result.returncode == 0

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
87% confidence
Finding

This markdown file documents how to run an end-to-end workflow that creates many output directories and files, and the embedded code includes overwrite behavior such as --force for ligand preparation and direct writes of summaries, configs, and exported structures. The README-style usage and overview sections do not include any user warning about these persistent filesystem changes or advising users to choose an isolated output directory.

Content

No source excerpt is available for this finding.