Back to skill

Security audit

Pharmaclaw Chemistry Query

Security checks for vulnerabilities and agentic risk

Overview

This chemistry skill mostly matches its stated purpose, but it has under-contained file write/read behavior and an auto-downloaded executable dependency that users should review before installing.

Install only if you are comfortable with a chemistry skill that can contact public chemistry and biomedical APIs, write output files, and download a checksum-pinned OPSIN Java JAR on first use. Run it in a restricted workspace, avoid exposing the Gradio UI to untrusted users, and do not pass arbitrary local paths to --input_file or --output until those paths are constrained.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/opsin_name_to_smiles.py:43
Finding

Unrestricted Arbitrary File Overwrite Through the OPSIN Output Argument

Content
View full analysis

Vulnerability Details

File Location: scripts/opsin_name_to_smiles.py, lines 43-59
Vulnerability Type: User-controlled output path and arbitrary file overwrite
Risk Level: Medium

python
def main():
    parser = argparse.ArgumentParser(description="OPSIN IUPAC name to SMILES")
    parser.add_argument("--name", required=True, help="IUPAC/systematic name")
    parser.add_argument("--output", help="Output SMILES file")
    args = parser.parse_args()

    jar_path = os.path.join(os.path.dirname(__file__), "opsin.jar")
    ensure_opsin(jar_path)

    cmd = ["java", "-jar", jar_path, "--stdin", "--output", "smiles"]
    proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    stdout, stderr = proc.communicate(input=args.name)
    if proc.returncode != 0:
        print(json.dumps({"error": stderr.strip()}), file=sys.stderr)
        sys.exit(1)
    
    smiles = stdout.strip()
    result = {"name": args.name, "smiles": smiles}
    print(json.dumps(result))
    if args.output:
        with open(args.output, 'w') as f:
            f.write(smiles)

Technical Analysis

The --output argument is controlled by the caller and is passed directly to open(..., 'w'). The script does not canonicalize or constrain the path, reject absolute paths or .. traversal, restrict extensions, check for symbolic links, or confine output to a dedicated directory.

Opening a path in write mode truncates an existing file before writing the generated SMILES. Consequently, a caller can target any file writable by the process. The content is constrained to OPSIN output, but an attacker can select a valid chemical name that produces attacker-influenced SMILES text. Even when useful replacement content cannot be produced, truncation or corruption of a targeted file may still cause denial of service.

This behavior also conflicts with the security-hardening ...[truncated 1373 chars]

Remediation
View remediation

Remediation Suggestions

  • Confine generated files to a dedicated output directory controlled by the application.
  • Resolve both the permitted directory and requested destination with os.path.realpath() or pathlib.Path.resolve(), then verify that the destination remains inside the permitted directory.
  • Reject absolute paths and traversal components when arbitrary paths are unnecessary.
  • Restrict output to an expected extension such as .smi or .txt.
  • Reject symbolic-link destinations and ensure that parent directories are not symbolic links.
  • Prefer exclusive creation with mode x, or perform atomic replacement only after validating the destination.
  • Run the Skill under a minimally privileged account with no write access to sensitive files.
  • Reuse a single, consistently tested output-path validation function across all scripts.

A hardened pattern should validate the resolved path before opening it:

python
from pathlib import Path

output_dir = (Path(__file__).resolve().parent / "output").resolve()
output_dir.mkdir(mode=0o700, exist_ok=True)

requested = Path(args.output).name
destination = (output_dir / requested).resolve()

if destination.parent != output_dir:
    raise ValueError("Output must remain within the output directory")
if destination.suffix.lower() not in {".smi", ".txt"}:
    raise ValueError("Unsupported output extension")
if destination.is_symlink():
    raise ValueError("Symbolic-link outputs are not permitted")

with destination.open("x", encoding="utf-8") as output_file:
    output_file.write(smiles)

T09 · Insecure Skill Coding Practices

Warning
Location
chem_ui.py:25
Finding

Cross-Request File Race Caused by a Shared Visualization Filename

Content
View full analysis

Vulnerability Details

File Location: chem_ui.py, lines 25-40
Vulnerability Type: Insecure shared temporary file and race condition
Risk Level: Medium

python
def analyze(smiles):
    smiles = _validate_smiles(smiles)
    # Props
    proc = subprocess.run(["python3", "rdkit_mol.py", "--smiles", smiles, "--action", "props"], cwd=WORK_DIR, capture_output=True, text=True)
    props = json.loads(proc.stdout)
    
    # Draw PNG
    subprocess.run(["python3", "rdkit_mol.py", "--smiles", smiles, "--action", "draw", "--output", "mol.png"], cwd=WORK_DIR)
    img = Image.open(os.path.join(WORK_DIR, "mol.png"))
    
    # ADMET
    proc = subprocess.run(["python3", "admet_predict.py", "--smiles", smiles], cwd=WORK_DIR, capture_output=True, text=True)
    admet = json.loads(proc.stdout)
    
    return props, img, admet

Technical Analysis

Every invocation of the Gradio handler writes its visualization to the same predictable file, scripts/mol.png, and subsequently reopens that shared path. There is no request-specific filename, private temporary directory, locking, or atomic association between the subprocess output and the image returned to the caller.

Under concurrent requests, one request can replace mol.png after another request generates its image but before that request opens or fully consumes it. The returned properties and ADMET result can therefore describe one molecule while the returned image depicts another.

The predictable path also introduces a symbolic-link risk if another actor with access to the working directory can create or replace scripts/mol.png. The drawing subprocess would then write through that path with the privileges of the server process.

The drawing subprocess return code is not checked. If drawing fails, the handler may open a stale image left by an earlier request, creating another route for cross-request result confusion.

Attack Path

  1. A victi ...[truncated 1506 chars]
Remediation
View remediation

Remediation Suggestions

  • Create a private temporary directory for each invocation with tempfile.TemporaryDirectory().
  • Generate a unique image path inside that directory rather than using mol.png.
  • Pass an absolute, validated request-specific path to the drawing subprocess.
  • Check returncode and parse errors before attempting to open the image.
  • Load the image fully and copy it into memory before the temporary directory is removed.
  • Avoid writable generated files inside the source-code directory.
  • Configure restrictive directory permissions and reject symbolic-link destinations.
  • Add request concurrency tests to verify that properties, ADMET data, and images remain associated with the same input.

Example hardened structure:

python
import sys
import tempfile
from pathlib import Path

def analyze(smiles):
    smiles = _validate_smiles(smiles)

    with tempfile.TemporaryDirectory(prefix="chem-ui-") as temp_dir:
        image_path = Path(temp_dir) / "molecule.png"

        proc = subprocess.run(
            [
                sys.executable,
                "rdkit_mol.py",
                "--smiles",
                smiles,
                "--action",
                "draw",
                "--output",
                str(image_path),
            ],
            cwd=WORK_DIR,
            capture_output=True,
            text=True,
            timeout=30,
        )
        if proc.returncode != 0:
            raise RuntimeError(f"Drawing failed: {proc.stderr.strip()}")

        with Image.open(image_path) as opened_image:
            image = opened_image.copy()

    return image

The same subprocess return-code and timeout checks should also be applied consistently to the property and ADMET subprocesses.

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (51)

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding

A broad set of advertised chemistry features that are absent in code, combined with narrower hardcoded reaction behavior, signals an unreliable and potentially misleading skill contract. That is dangerous because downstream agents may route sensitive or high-trust chemistry tasks into a tool whose real behavior and external interactions are not what reviewers evaluated.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

A broad set of advertised chemistry features that are absent in code, combined with narrower hardcoded reaction behavior, signals an unreliable and potentially misleading skill contract. That is dangerous because downstream agents may route sensitive or high-trust chemistry tasks into a tool whose real behavior and external interactions are not what reviewers evaluated.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

A broad set of advertised chemistry features that are absent in code, combined with narrower hardcoded reaction behavior, signals an unreliable and potentially misleading skill contract. That is dangerous because downstream agents may route sensitive or high-trust chemistry tasks into a tool whose real behavior and external interactions are not what reviewers evaluated.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

A broad set of advertised chemistry features that are absent in code, combined with narrower hardcoded reaction behavior, signals an unreliable and potentially misleading skill contract. That is dangerous because downstream agents may route sensitive or high-trust chemistry tasks into a tool whose real behavior and external interactions are not what reviewers evaluated.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

A broad set of advertised chemistry features that are absent in code, combined with narrower hardcoded reaction behavior, signals an unreliable and potentially misleading skill contract. That is dangerous because downstream agents may route sensitive or high-trust chemistry tasks into a tool whose real behavior and external interactions are not what reviewers evaluated.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

A broad set of advertised chemistry features that are absent in code, combined with narrower hardcoded reaction behavior, signals an unreliable and potentially misleading skill contract. That is dangerous because downstream agents may route sensitive or high-trust chemistry tasks into a tool whose real behavior and external interactions are not what reviewers evaluated.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding

A broad set of advertised chemistry features that are absent in code, combined with narrower hardcoded reaction behavior, signals an unreliable and potentially misleading skill contract. That is dangerous because downstream agents may route sensitive or high-trust chemistry tasks into a tool whose real behavior and external interactions are not what reviewers evaluated.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding

A broad set of advertised chemistry features that are absent in code, combined with narrower hardcoded reaction behavior, signals an unreliable and potentially misleading skill contract. That is dangerous because downstream agents may route sensitive or high-trust chemistry tasks into a tool whose real behavior and external interactions are not what reviewers evaluated.

Content

No source excerpt is available for this finding.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
### `scripts/advanced_chem.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
### `scripts/advanced_chem.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
### `scripts/advanced_chem.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
### `scripts/advanced_chem.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
### `scripts/advanced_chem.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
### `scripts/advanced_chem.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
### `scripts/advanced_chem.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
### `scripts/advanced_chem.py`

Description-Behavior Mismatch

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

This script performs PubMed literature searches even though the skill manifest describes PubChem/RDKit chemistry-query functionality. Scope divergence matters because it expands data flows and capabilities beyond what operators and users expect, which can bypass review assumptions and transmit user research queries to a different external service.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding

The skill advertises and documents capabilities that require shell execution, network access, and file writing, but it does not declare any explicit tool scope such as allowed-tools or permissions. This creates an authorization gap where a host agent may grant broader runtime access than users expect, increasing the chance of unintended external calls, file creation, or command execution.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

An overly broad trigger can cause the skill to activate on general chemistry discussions, increasing accidental invocation of networked, shell-capable, or file-writing functionality. In this context, broad matching is more dangerous because the skill includes external queries, synthesis-related features, and optional binary download behavior.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The skill does not clearly warn in its main markdown description that it performs external network queries and may auto-download an external JAR on first use. Users and orchestration layers may therefore invoke it without understanding that it will contact third-party services and retrieve executable dependencies, which is a meaningful transparency and consent failure.

Content

No source excerpt is available for this finding.

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 · chem_ui.py (reported line 30)May include surrounding context.

python
def analyze(smiles):
    smiles = _validate_smiles(smiles)
    # Props
    proc = subprocess.run(["python3", "rdkit_mol.py", "--smiles", smiles, "--action", "props"], cwd=WORK_DIR, capture_output=True, text=True)
    props = json.loads(proc.stdout)
    
    # Draw PNG

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
73% confidence
Finding

The code writes output to a fixed filename (mol.png) in a shared working directory and then opens that file without validating subprocess success or isolating requests. In a multi-user Gradio service, concurrent requests can overwrite each other's files, causing data leakage, race conditions, or unintended file access through symlink/preexisting-file abuse if the working directory is writable by other processes.

Content

Scanner excerpt · chem_ui.py (reported line 34)May include surrounding context.

python
props = json.loads(proc.stdout)
    
    # Draw PNG
    subprocess.run(["python3", "rdkit_mol.py", "--smiles", smiles, "--action", "draw", "--output", "mol.png"], cwd=WORK_DIR)
    img = Image.open(os.path.join(WORK_DIR, "mol.png"))
    
    # ADMET

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The UI describes the tool as PubChem/RDKit analysis, but the implementation also runs an ADMET prediction script. This undisclosed capability increases trust and consent risk because users and platform operators may not expect additional local model/script execution or the handling of potentially sensitive chemical inputs for a separate purpose.

Content

No source excerpt is available for this finding.

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 · chem_ui.py (reported line 38)May include surrounding context.

python
img = Image.open(os.path.join(WORK_DIR, "mol.png"))
    
    # ADMET
    proc = subprocess.run(["python3", "admet_predict.py", "--smiles", smiles], cwd=WORK_DIR, capture_output=True, text=True)
    admet = json.loads(proc.stdout)
    
    return props, img, admet

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The chemspace mode accepts an arbitrary local input_file path and reads it directly from the filesystem with only existence and length checks. In an agent environment, this expands the skill from chemistry analysis into local file access, which can expose sensitive data if an attacker induces the agent to read unintended files within its accessible environment.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.