Back to skill

Security audit

STEP Parts

Security checks for vulnerabilities and agentic risk

Overview

The skill has a legitimate CAD-parts purpose, but its downloader can write downloaded files outside the intended output folder in some user-directed cases.

Review this skill before installing. Use the default step.parts origin when possible, avoid custom mirrors unless you trust them, do not pass path components to --filename, avoid --overwrite except for files you explicitly intend to replace, and treat downloaded CAD files as untrusted until checksums verify.

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/download_step_part.py:119
Finding

Unrestricted Download Filename Permits Writes Outside the Output Directory

Content
View full analysis

Vulnerability Details

File Location: scripts/download_step_part.py, lines 119-145
Vulnerability Type: Path traversal and arbitrary file write
Risk Level: Medium

python
def filename_for(part: dict[str, Any], requested_filename: str | None, allow_requested: bool) -> str:
    if requested_filename and allow_requested:
        return requested_filename
    step_url = str(part.get("stepUrl") or "")
    name = Path(urllib.parse.urlparse(step_url).path).name
    if name:
        return name
    return f"{part['id']}.step"


def step_download_url(part: dict[str, Any], origin: str) -> str:
    step_url = part.get("stepUrl")
    if not step_url:
        raise SystemExit(f"Part {part.get('id', '<unknown>')} does not include stepUrl.")
    return urllib.parse.urljoin(f"{origin_url(origin)}/", str(step_url))


def write_download(part: dict[str, Any], args: argparse.Namespace, allow_requested_filename: bool) -> dict[str, Any]:
    step_url = step_download_url(part, args.origin)

    out_dir = Path(args.out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    path = out_dir / filename_for(part, args.filename, allow_requested_filename)
    if path.exists() and not args.overwrite:
        raise SystemExit(f"Refusing to overwrite existing file: {path}")

    data = request(step_url, args.timeout)
    path.write_bytes(data)

Technical Analysis

The value accepted through --filename is returned without validation. It may therefore contain an absolute path or parent-directory components such as ../.

With pathlib, joining a base directory to an absolute second path discards the base directory. Relative traversal components can similarly resolve outside args.out_dir. The implementation does not resolve the resulting path and verify that it remains under the intended output directory before calling write_bytes().

Existing files are protected by default, but supplying `--overw ...[truncated 1540 chars]

Remediation
View remediation

Remediation Suggestions

  1. Restrict --filename to a basename:
    python
    requested = Path(requested_filename)
    if requested.is_absolute() or requested.name != requested_filename:
        raise SystemExit("--filename must be a plain filename without directory components.")
    
  2. Resolve both paths and enforce containment before writing:
    python
    out_dir = Path(args.out_dir).resolve()
    path = (out_dir / safe_filename).resolve()
    if path.parent != out_dir:
        raise SystemExit("Download path escapes the output directory.")
    
  3. Reject . and .., path separators for all supported platforms, null characters, and platform-specific absolute path forms.
  4. Consider restricting writes to a dedicated download directory rather than accepting arbitrary output directories.
  5. Retain the default no-overwrite behavior and require explicit, separately confirmed authorization before replacing an existing file.
  6. Use exclusive temporary-file creation followed by an atomic rename after successful validation to reduce race-condition and partial-write risks.

T09 · Insecure Skill Coding Practices

Note
Location
scripts/download_step_part.py:144
Finding

Downloaded File Is Persisted Before Integrity Verification

Content
View full analysis

Vulnerability Details

File Location: scripts/download_step_part.py, lines 144-153
Vulnerability Type: Improper integrity-validation ordering
Risk Level: Low

python
    data = request(step_url, args.timeout)
    path.write_bytes(data)

    actual_sha256 = hashlib.sha256(data).hexdigest()
    expected_sha256 = part.get("sha256")
    checksum_ok = expected_sha256 is None or expected_sha256 == actual_sha256
    if not checksum_ok:
        raise SystemExit(
            f"Checksum mismatch for {path}: expected {expected_sha256}, got {actual_sha256}",
        )

Technical Analysis

The downloader writes remote content to its final destination before comparing its SHA-256 digest with the digest in the part record. If verification fails, the script exits but does not remove the invalid file. The untrusted bytes therefore remain at a path that downstream software may discover or process.

Furthermore, expected_sha256 is None is treated as successful. This behavior is consistent with the documentation stating that verification occurs “when present,” but it means records from a user-selected or compromised API origin can omit the digest and bypass integrity checking entirely.

SHA-256 comparison protects integrity only when the expected value comes from a trusted API response. It does not independently protect against a malicious custom origin that controls both the asset and expected digest.

Attack Path

  1. An asset endpoint, redirect destination, network-adjacent compromised service, or user-selected mirror returns altered content.
  2. The downloader receives the content and immediately writes it to the final .step path.
  3. The computed digest does not match the expected digest.
  4. The script reports an error and terminates.
  5. The invalid file remains at the destination.
  6. A subsequent workflow, file watcher, CAD viewer, or user may process the residual file despite the failed downloa ...[truncated 814 chars]
Remediation
View remediation

Remediation Suggestions

  1. Compute and compare the SHA-256 digest before placing data at the final destination.
  2. Write downloads to a securely created temporary file in the destination directory.
  3. Verify the temporary file and atomically rename it to the final destination only after successful validation.
  4. Delete the temporary file on checksum mismatch, download failure, interruption, or any other exception.
  5. For the canonical service, consider refusing downloads without a valid SHA-256 value rather than treating a missing digest as successful.
  6. If checksum-less mirrors must remain supported, require an explicit opt-in flag and clearly mark the resulting artifact as unverified.
  7. Keep custom origins user-authorized and avoid implying that a checksum supplied by the same untrusted origin provides independent authenticity.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (7)

Self-Modification

High
Category
Rogue Agent
Confidence
88% confidence
Finding

The script writes downloaded content to a path derived from untrusted remote metadata (stepUrl) or a user-supplied filename without constraining the final path to remain inside out-dir. An attacker controlling the API response or a malicious/compromised origin could supply a filename like ../../target or an absolute-path-like value, causing arbitrary file overwrite when --overwrite is used or when the target file does not already exist.

Content

Scanner excerpt · scripts/download_step_part.py (reported line 138)May include surrounding context.

python
out_dir.mkdir(parents=True, exist_ok=True)
    path = out_dir / filename_for(part, args.filename, allow_requested_filename)
    if path.exists() and not args.overwrite:
        raise SystemExit(f"Refusing to overwrite existing file: {path}")

    data = request(step_url, args.timeout)
    path.write_bytes(data)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill explicitly instructs the agent to perform network access to api.step.parts and to download STEP files to local storage, but it does not declare any permissions or allowed-tools scope. That creates an authority mismatch: reviewers and runtime policy cannot easily verify or constrain the skill's expected capabilities, increasing the risk of unintended network/file operations or policy bypass through an over-privileged agent environment.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · references/step-parts-api.md (reported line 30)May include surrounding context.

Examples:

text
https://api.step.parts/v1/parts?q=M3&tag=screw&page=2
https://api.step.parts/v1/parts?pageSize=100
https://api.step.parts/v1/parts?category=fastener&family=socket-head-cap-screw&standard=ISO%204762
https://api.step.parts/v1/parts?q=lengthMm%2012

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · references/step-parts-api.md (reported line 31)May include surrounding context.

Examples:

text
https://api.step.parts/v1/parts?q=M3&tag=screw&page=2
https://api.step.parts/v1/parts?pageSize=100
https://api.step.parts/v1/parts?category=fastener&family=socket-head-cap-screw&standard=ISO%204762
https://api.step.parts/v1/parts?q=lengthMm%2012

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · references/step-parts-api.md (reported line 32)May include surrounding context.

Examples:

text
https://api.step.parts/v1/parts?q=M3&tag=screw&page=2
https://api.step.parts/v1/parts?pageSize=100
https://api.step.parts/v1/parts?category=fastener&family=socket-head-cap-screw&standard=ISO%204762
https://api.step.parts/v1/parts?q=lengthMm%2012

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · references/step-parts-api.md (reported line 33)May include surrounding context.

Examples:

text
https://api.step.parts/v1/parts?q=M3&tag=screw&page=2
https://api.step.parts/v1/parts?pageSize=100
https://api.step.parts/v1/parts?category=fastener&family=socket-head-cap-screw&standard=ISO%204762
https://api.step.parts/v1/parts?q=lengthMm%2012

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/download_step_part.py (reported line 91)May include surrounding context.

python
if args.query:
        params.append(("q", args.query))
    for key in ("tag", "category", "family", "standard"):
        for value in getattr(args, key):
            params.append((key, value))
    return fetch_json(build_url(args.origin, "/v1/parts", params), args.timeout)

Static analysis

No suspicious patterns detected.