T09 · Insecure Skill Coding Practices
- 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: Mediumpython 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
--outputargument is controlled by the caller and is passed directly toopen(..., '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()orpathlib.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
.smior.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)
