Back to skill

Security audit

Amazon SEND CATCH Invoice

Security checks for vulnerabilities and agentic risk

Overview

This skill has a coherent invoice-generation purpose, but it handles portal credentials, cached tokens, arbitrary portal URLs, and order-placing uploads without enough transport and destination safeguards.

Install only if you trust the publisher and can run it in a constrained project environment. Use a verified HTTPS CATCH portal URL, avoid exposing broad ERP or database credentials to the agent, review generated invoices before upload, and treat upload as an order-placement action requiring explicit approval.

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 (6)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/catch_api.py:59
Finding
Credentials, Authentication Tokens, and Customs Invoices May Be Transmitted over Plaintext HTTP<![CDATA[ ## Vulnerability Details **File Location**: `scripts/catch_api.py`, lines 59-71 and 93-119 **Vulnerability Type**: Sensitive-data transmission without enforced transport security **Risk Level**: High ### Vulnerable Code ```python BASE = os.environ.get("CATCH_BASE_URL", "").rstrip("/") def _base() -> str: if not BASE: sys.exit("set $CATCH_BASE_URL to the portal origin, e.g. https://portal.example.com") return BASE ``` ```python def login() -> str: user, password = os.environ.get("CATCH_USER"), os.environ.get("CATCH_PASS") if not (user and password): sys.exit("set $CATCH_USER and $CATCH_PASS") r = _post("/v1/user/login", {"username": user, "password": password}, auth=False) ``` ```python def _req(method, path, data=None, headers=None, auth=True): h = dict(COMMON) if auth: token = load_token() if not token: sys.exit("no token — run `python3 catch_api.py login` or set $CATCH_TOKEN") h["x-token"] = token h.update(headers or {}) req = urllib.request.Request(_base() + path, data=data, headers=h, method=method) try: with urllib.request.urlopen(req, timeout=120) as resp: return json.loads(resp.read()) ``` The related documentation explicitly describes upload “over pure HTTP” and allows a configurable portal origin: ```markdown export CATCH_BASE_URL=https://portal.example.com ``` ### Technical Analysis Uploading customs invoices and authenticating to the forwarder portal are necessary for the declared functionality. However, the implementation accepts any value in `CATCH_BASE_URL` and does not verify that its scheme is HTTPS. If the variable is accidentally or maliciously configured with an `http://` URL, the following information is transmitted without encryption: - Portal username and password during `/v1/user/login` - The reusable `x-token` authentication token - Complete customs invoice files - FBA shipment and box identifiers - Amazon r ...[truncated 1567 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse `CATCH_BASE_URL` with `urllib.parse.urlsplit`. 2. Reject every scheme other than `https`. 3. Reject URLs containing embedded credentials, fragments, or unexpected paths. 4. Require an explicit opt-in flag for local development endpoints, limited to loopback addresses. 5. Consider an allowlist of approved forwarder portal hostnames. 6. Resolve and validate redirects so authenticated requests cannot be redirected to another host. 7. Never forward `x-token` across an origin change. 8. Document that credentials and invoices must only be transmitted to a verified CATCH domain. 9. Add tests confirming that `http://`, malformed URLs, and untrusted hosts are rejected before credentials are loaded or requests are constructed. Example validation: ```python from urllib.parse import urlsplit def _base() -> str: if not BASE: sys.exit("set $CATCH_BASE_URL") parsed = urlsplit(BASE) if parsed.scheme != "https" or not parsed.hostname: sys.exit("CATCH_BASE_URL must be a valid HTTPS origin") if parsed.username or parsed.password or parsed.query or parsed.fragment: sys.exit("CATCH_BASE_URL must contain only a trusted HTTPS origin") return f"https://{parsed.netloc}" ``` ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/fetch_images.py:77
Finding
Arbitrary Image URLs Enable Server-Side Request Forgery and Unbounded Response Downloads<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch_images.py`, lines 77-89 **Vulnerability Type**: SSRF and uncontrolled resource consumption **Risk Level**: High ### Vulnerable Code ```python def fetch(key: str, url: str, size=DEFAULT_SIZE, force=False) -> Path | None: from PIL import Image out = cache_dir() / f"{key}.bmp" if out.exists() and not force: return out try: if url.startswith(("http://", "https://")): req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=15) as r: data = r.read() img = Image.open(io.BytesIO(data)) else: img = Image.open(Path(url).expanduser()) # local file path ``` ### Technical Analysis The image-map input completely controls the URL passed to `urllib.request.urlopen`. The implementation: - Allows both HTTP and HTTPS. - Does not restrict destination hostnames. - Does not reject loopback, link-local, private, multicast, or reserved IP addresses. - Follows HTTP redirects without validating the redirected destination. - Reads the entire response into memory with `r.read()` and imposes no byte limit. - Does not validate the response `Content-Type` before downloading it. - Passes downloaded content to Pillow, increasing exposure to malformed-image parser vulnerabilities and decompression bombs. This network capability is broader than required. The declared function only needs product images from trusted catalog/CDN locations or explicit local files. ### Attack Path 1. An attacker supplies or modifies `images.json`. 2. The attacker sets an image URL to an internal service, for example: - `http://127.0.0.1:PORT/...` - `http://169.254.169.254/...` - A private network address such as `http://10.0.0.5/...` - A public URL that redirects to one of those destinations 3. The user or agent runs `fetch_images.py`. 4. The process issues a request ...[truncated 929 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Allow only HTTPS for remote images. 2. Restrict remote hosts to trusted Amazon image CDN domains or a user-approved allowlist. 3. Resolve hostnames and reject all loopback, private, link-local, multicast, unspecified, and reserved IP addresses. 4. Repeat destination validation after every redirect, or disable redirects. 5. Stream downloads in bounded chunks instead of calling an unlimited `read()`. 6. Enforce a conservative maximum response size before and during download. 7. Require an expected image `Content-Type`. 8. Configure Pillow limits and treat `DecompressionBombWarning` as an error. 9. Validate decoded image dimensions before conversion. 10. Consider separating local-file and remote-URL input modes so remote access must be explicitly enabled. Example bounded download logic: ```python MAX_IMAGE_BYTES = 10 * 1024 * 1024 with urllib.request.urlopen(req, timeout=15) as response: content_type = response.headers.get_content_type() if not content_type.startswith("image/"): raise ValueError("remote resource is not an image") data = response.read(MAX_IMAGE_BYTES + 1) if len(data) > MAX_IMAGE_BYTES: raise ValueError("image exceeds maximum allowed size") ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch_images.py:77
Finding
Unsanitized Image Keys Permit Filesystem Path Traversal and Arbitrary BMP Writes<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch_images.py`, lines 77-91 and 112-116 **Vulnerability Type**: Path traversal through attacker-controlled cache filenames **Risk Level**: Medium ### Vulnerable Code ```python def fetch(key: str, url: str, size=DEFAULT_SIZE, force=False) -> Path | None: from PIL import Image out = cache_dir() / f"{key}.bmp" if out.exists() and not force: return out try: if url.startswith(("http://", "https://")): req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=15) as r: data = r.read() img = Image.open(io.BytesIO(data)) else: img = Image.open(Path(url).expanduser()) # local file path fit_square(img.convert("RGB"), size).save(out, "BMP") patch_bmp_dpi(out) ``` ```python mapping = json.loads(Path(args.images).read_text(encoding="utf-8")) ok = 0 for key, url in mapping.items(): p = fetch(key, url, args.size, args.force) ``` ### Technical Analysis The JSON object key is inserted directly into a path: ```python cache_dir() / f"{key}.bmp" ``` No validation rejects path separators, `..` components, absolute paths, drive prefixes, or platform-specific special names. `pathlib` does not guarantee that this construction remains below `cache_dir()` when the right-hand value is absolute or contains traversal components. With `--force`, an existing destination can be overwritten. The written content is constrained to a BMP generated by Pillow, but an attacker can still place or replace a `.bmp` file at an unintended writable location. Parent directories must already exist. ### Attack Path 1. An attacker supplies an image mapping with a malicious key containing traversal components or an absolute path. 2. The mapping points to any valid image the script can decode. 3. The user invokes `fetch_images.py`, optionally with `--force`. ...[truncated 694 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Define a strict format for product keys, such as ASCII letters, digits, dots, underscores, and hyphens. 2. Reject empty keys, `.` and `..`, path separators, drive prefixes, and absolute paths. 3. Resolve the final output path and verify that it remains under the resolved cache directory. 4. Use a deterministic hash of the logical product key as the physical filename. 5. Store a separate key-to-filename index if preserving original identifiers is required. 6. Avoid overwriting existing files unless the user explicitly confirms the resolved destination. Example: ```python import hashlib def cache_path_for_key(key: str) -> Path: if not isinstance(key, str) or not key: raise ValueError("invalid product key") digest = hashlib.sha256(key.encode("utf-8")).hexdigest() root = cache_dir().resolve() destination = (root / f"{digest}.bmp").resolve() if root not in destination.parents: raise ValueError("cache path escaped cache directory") return destination ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/build_shipments.py:56
Finding
Shipment Identifiers Permit Output Path Traversal in Shipment File Generation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/build_shipments.py`, lines 56-77 **Vulnerability Type**: Path traversal through shipment-controlled output filenames **Risk Level**: Medium ### Vulnerable Code ```python out_dir = Path(args.out) out_dir.mkdir(parents=True, exist_ok=True) for s in plan["shipments"]: fba, fc = s["fba_shipment_id"], s["warehouse_code"] box_codes = s.get("box_codes") or [] n_boxes = s.get("boxes", len(box_codes) or 1) if len(box_codes) < n_boxes: print(f"! {fba}: {len(box_codes)} box_codes for {n_boxes} boxes — " "boxes with no code will be blank on the invoice") boxes = [{ "box_seq": i + 1, "fba_shipment_id": fba, "reference_id": s.get("amazon_reference_id", ""), "box_code": box_codes[i] if i < len(box_codes) else "", "warehouse_code": fc, "items": items, } for i in range(n_boxes)] doc = {"ticket": {**ticket, "label": f"{fba}_{fc}"}, "boxes": boxes} path = out_dir / f"{fba}_{fc}.json" path.write_text(json.dumps(doc, ensure_ascii=False, indent=2), encoding="utf-8") ``` ### Technical Analysis The FBA shipment ID and warehouse code come from the plan-result JSON and are used directly in the output filename. There is no validation that these values match expected identifier formats. Path separators, traversal components, absolute paths, or platform-specific path prefixes can cause the final destination to escape `--out`. `Path.write_text` overwrites an existing file by default. The content is constrained to generated JSON, but the destination is attacker-controlled within the current user’s filesystem permissions. ### Attack Path 1. An attacker supplies or modifies the inbound plan-result JSON. 2. A shipment’s `fba_shipment_id` or `warehouse_code` includes path separators, traversal components, or an absolute path. 3. The user runs `build_shipments.py`. 4. The expression `out_dir / f"{fba}_{fc}.json"` resolv ...[truncated 488 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate `fba_shipment_id` and `warehouse_code` against strict expected formats. 2. Reject all path separators and traversal components. 3. Resolve the final path and verify that it is a direct child of the resolved output directory. 4. Use exclusive file creation or require confirmation before overwriting existing files. 5. Treat plan-result files as untrusted input even when normally generated by a companion Skill. Example: ```python import re IDENTIFIER = re.compile(r"^[A-Za-z0-9_-]+$") def safe_identifier(value: str, field: str) -> str: if not IDENTIFIER.fullmatch(value): raise ValueError(f"invalid {field}") return value fba = safe_identifier(s["fba_shipment_id"], "fba_shipment_id") fc = safe_identifier(s["warehouse_code"], "warehouse_code") root = out_dir.resolve() path = (root / f"{fba}_{fc}.json").resolve() if path.parent != root: raise ValueError("output path escaped shipment directory") ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/generate_invoice.py:261
Finding
Shipment Labels and Product Keys Can Escape Intended Invoice and Image Paths<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate_invoice.py`, lines 261-269 and 281-286 **Vulnerability Type**: Path traversal and unintended local-file access **Risk Level**: Medium ### Vulnerable Code ```python if "image" in cols: img = cache_dir() / f"{item['product_key']}.bmp" if img.exists(): try: place_square_bitmap(ws, img, row, cols["image"], img_px) except Exception as e: # noqa: BLE001 print(f"! image {img.name}: {e}") ``` ```python stamp = date.today().strftime("%Y-%m-%d") channel = str(ticket.get("channel", "")).replace("/", "-") label = ticket.get("label") or fba_id parts = [stamp] + ([channel] if channel else []) + [label, f"{len(boxes)}box"] out_path = out_dir / ("_".join(parts) + ".xls") wb.save(str(out_path)) ``` ### Technical Analysis Two filesystem paths are influenced by shipment data: 1. `product_key` is inserted directly into the image-cache lookup path. 2. `ticket.label` is inserted directly into the generated invoice filename. Only forward slashes in `channel` are replaced. The label and product keys are not validated for traversal components, absolute paths, Windows separators, drive prefixes, or special names. A malicious product key can make the generator inspect a BMP outside the cache directory. If the referenced file is a valid bitmap accepted by `xlwt`, its contents may be embedded into the resulting invoice. A malicious label can cause the generated invoice to be saved outside `out_dir`, subject to path construction and existing parent directories. ### Attack Path 1. An attacker supplies or modifies a shipment JSON file. 2. For local-file access, the attacker sets `product_key` to a traversal path pointing to an existing BMP file. 3. The generator resolves that path relative to the image cache and attempts to embed the file into the invoice. 4. For unintended writes, the attacker sets `ticket.label` to a value containing traversal compo ...[truncated 680 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Apply the same strict product-key normalization used by `fetch_images.py`. 2. Use hashed physical cache filenames rather than raw product keys. 3. Validate `ticket.label` and `channel` against a conservative filename-safe character set. 4. Normalize both `/` and `\` and reject absolute or drive-qualified values. 5. Resolve image and output paths and confirm they remain under their designated root directories. 6. Refuse to overwrite an existing invoice unless explicitly requested. 7. Ensure shipment files from companion tools are validated against a schema before use. Example output validation: ```python import re SAFE_LABEL = re.compile(r"^[A-Za-z0-9_.-]+$") if not SAFE_LABEL.fullmatch(str(label)): raise ValueError("shipment label contains unsafe filename characters") root = out_dir.resolve() out_path = (root / ("_".join(parts) + ".xls")).resolve() if out_path.parent != root: raise ValueError("invoice path escaped records directory") ``` ]]>

T08 · Insecure Dependencies

Note
Location
SKILL.md:58
Finding
Third-Party Python Dependencies Are Installed without Version Pinning or Integrity Verification<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md`, line 58 **Vulnerability Type**: Unpinned and unverifiable dependency installation **Risk Level**: Low ### Vulnerable Code ```bash pip3 install xlrd xlwt xlutils pillow ``` ### Technical Analysis The installation instructions retrieve the latest available versions of four packages without: - Exact version pins - Cryptographic hashes - A lock file - A reviewed package index configuration - Reproducible environment instructions The referenced package names are established packages and no typosquatting or malicious package was identified in the reviewed files. Nevertheless, unconstrained installation means the code reviewed during this audit may later execute against materially different dependency versions. Python package installation may execute package build logic. A compromised upstream release, compromised package index account, dependency substitution through a malicious configured index, or incompatible future version could execute code during installation or when imported. ### Attack Path 1. A user follows the documented `pip3 install` command. 2. Pip resolves whatever package versions and package index are active at installation time. 3. A compromised release, malicious alternate index, or future incompatible dependency is selected. 4. Installation-time build logic or imported package code executes with the user’s privileges. 5. The malicious or altered dependency gains access to product data, shipment files, generated invoices, and environment variables available to the process. ### Impact Assessment A compromised dependency can execute arbitrary code with the invoking user’s privileges. It may access local files and credentials, including `CATCH_USER`, `CATCH_PASS`, or `CATCH_TOKEN` when commands are run in an environment containing them. This is a supply-chain hardening deficiency rather than evidence that the currently named packages are malicious. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add a reviewed requirements file with exact versions. 2. Generate and verify hashes for all direct and transitive dependencies. 3. Install with `pip install --require-hashes -r requirements.txt`. 4. Use a trusted package index and disable unapproved extra indexes. 5. Periodically update pins through a controlled dependency-review process. 6. Run dependency vulnerability scanning in CI. 7. Install dependencies inside a dedicated virtual environment with minimal filesystem and credential access. 8. Avoid exposing portal credentials in the environment while installing packages. Example: ```text # requirements.txt xlrd==<reviewed-version> --hash=sha256:<reviewed-hash> xlwt==<reviewed-version> --hash=sha256:<reviewed-hash> xlutils==<reviewed-version> --hash=sha256:<reviewed-hash> Pillow==<reviewed-version> --hash=sha256:<reviewed-hash> ``` ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (10)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The claimed behavior includes direct API retrieval and HTTP upload to a forwarder portal, but the finding says the implementation only reads local JSON and uses cached local files. Such discrepancies can bypass user expectations and security approvals, especially when a skill appears operationally capable of placing orders but may actually do something else or fail silently.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The claimed behavior includes direct API retrieval and HTTP upload to a forwarder portal, but the finding says the implementation only reads local JSON and uses cached local files. Such discrepancies can bypass user expectations and security approvals, especially when a skill appears operationally capable of placing orders but may actually do something else or fail silently.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The claimed behavior includes direct API retrieval and HTTP upload to a forwarder portal, but the finding says the implementation only reads local JSON and uses cached local files. Such discrepancies can bypass user expectations and security approvals, especially when a skill appears operationally capable of placing orders but may actually do something else or fail silently.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The claimed behavior includes direct API retrieval and HTTP upload to a forwarder portal, but the finding says the implementation only reads local JSON and uses cached local files. Such discrepancies can bypass user expectations and security approvals, especially when a skill appears operationally capable of placing orders but may actually do something else or fail silently.

Anti-Refusal Statement

High
Category
Anti-Refusal
Content
## 5. When the forwarder changes the template

They do, without warning, and a shifted column produces an invoice that looks
correct and is either rejected or — worse — accepted with values in the wrong
fields.
Confidence
80% confidence
Finding
Skill instructs the agent to omit warnings, disclaimers, or ethical commentary. Stripping safety caveats hides risk from the user and is a common jailbreak preamble.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill declares access to environment variables, filesystem, shell, and network-capable behavior but does not define any explicit tool scope or permission boundaries. In an agent setting, this creates unnecessary ambient authority and increases the chance that the skill could read secrets, modify files, or make outbound requests beyond what a user expects.

Vague Triggers

Medium
Confidence
94% confidence
Finding
The trigger list is very broad and includes generic phrases like customs invoice, declaration, and upload invoice, which increases the chance of accidental activation in contexts the user did not intend. Overbroad activation is risky for a skill that may handle credentials, local files, and outbound requests because it can be invoked on ambiguous prompts with sensitive consequences.

Context-Inappropriate Capability

Medium
Confidence
88% confidence
Finding
The documentation explicitly tells the skill operator to request endpoint details, database connection information, and credentials, then connect to the user's ERP, API, database, or spreadsheet source. That expands the skill from invoice generation/upload into broad data-access and credential-handling behavior, which increases the attack surface and creates a path for unnecessary access to sensitive internal systems if the skill is over-permissioned or misused.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
user, password = os.environ.get("CATCH_USER"), os.environ.get("CATCH_PASS")
    if not (user and password):
        sys.exit("set $CATCH_USER and $CATCH_PASS")
    r = _post("/v1/user/login", {"username": user, "password": password}, auth=False)
    token = (r.get("data") or {}).get("token")
    if not token:
        sys.exit(
Confidence
75% confidence
Finding
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The file contains fixed Chinese-language strings for labels and an Excel sheet name, but provides no indication that the skill is intentionally region-specific or that users can choose another language/locale. Per the policy, forcing a specific language without opt-in is a natural-language policy concern.

Static analysis

No suspicious patterns detected.