Back to skill

Security audit

Capacities Lookup

Security checks for vulnerabilities and agentic risk

Overview

This Capacities lookup skill mostly matches its stated purpose, but it can send the user's Capacities bearer token to an arbitrary configured API endpoint.

Review this skill before installing. It needs a Capacities API token and will make live API calls and write cache files under data/capacities/. Do not use a custom CAPACITIES_API_BASE_URL or apiBaseUrl unless you fully trust that endpoint, because the current code will attach your bearer token to it. Prefer setting only CAPACITIES_API_TOKEN and CAPACITIES_SPACE_ID for the official Capacities API.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/capacities_client.py:59
Finding
Unrestricted API Endpoint Can Expose the Capacities Bearer Token and Private Queries## Vulnerability Details **File Location**: `scripts/capacities_client.py`, lines 59–74 and 107–124 **Vulnerability Type**: Unvalidated authenticated API destination **Risk Level**: High ### Vulnerable Code ```python def load_config() -> dict[str, Any]: config: dict[str, Any] = { "apiBaseUrl": os.environ.get("CAPACITIES_API_BASE_URL", "https://api.capacities.io"), "timeoutMs": int(os.environ.get("CAPACITIES_TIMEOUT_MS", "15000")), "lookupCacheTtlSeconds": int(os.environ.get("CAPACITIES_LOOKUP_CACHE_TTL_SECONDS", "86400")), "verifySpacesOnSync": True, "defaultResultLimit": int(os.environ.get("CAPACITIES_DEFAULT_RESULT_LIMIT", "10")), "cacheSchemaVersion": 1, } if CONFIG_PATH.exists(): with CONFIG_PATH.open("r", encoding="utf-8") as f: file_config = json.load(f) config.update(file_config) ``` ```python def request(method: str, path: str, json_body: dict[str, Any] | None = None, retries: int = 2) -> dict[str, Any]: config = load_config() token = get_token() url = config["apiBaseUrl"].rstrip("/") + path timeout_seconds = max(int(config["timeoutMs"]) / 1000, 1) body_bytes = None headers = { "Authorization": f"Bearer {token}", "Accept": "application/json", } if json_body is not None: body_bytes = json.dumps(json_body).encode("utf-8") headers["Content-Type"] = "application/json" last_error: Exception | None = None for attempt in range(retries + 1): req = urllib.request.Request(url, data=body_bytes, headers=headers, method=method.upper()) try: with urllib.request.urlopen(req, timeout=timeout_seconds) as resp: ``` The sensitive lookup data sent through this request path is constructed at lines 164–165: ```python def lookup(space_id: str, search_term: str) -> dict[str, Any]: return request("POST", "/lookup", {"spaceId": space_id, "searchTerm": search_term}) ``` ### Technica ...[truncated 2232 chars]
Remediation
## Remediation Suggestions 1. **Pin the production origin** to `https://api.capacities.io` and reject other destinations by default. 2. **Parse and validate the URL** before creating an authenticated request: - Require the `https` scheme. - Require an explicitly approved hostname. - Reject embedded usernames or passwords. - Reject fragments, unexpected ports, and malformed URLs. 3. **Require explicit development opt-in** if custom endpoints are genuinely necessary, such as a separate `CAPACITIES_ALLOW_CUSTOM_API_BASE_URL=true` setting with a clear warning. 4. **Do not send production credentials to custom origins.** Require a separate development token variable when a non-production endpoint is enabled. 5. **Prevent configuration-file overrides from silently replacing security-sensitive defaults.** Validate `file_config` through an allowlisted schema rather than applying unrestricted `config.update(file_config)`. 6. **Avoid redirects across origins with credentials.** Ensure redirect handling cannot forward the bearer token to an unapproved host. 7. **Document the endpoint override and its risks** in `SKILL.md`. 8. **Revoke and rotate the token** if there is any indication that the endpoint setting was previously redirected or plain HTTP was used. A validation pattern should enforce the trusted endpoint before constructing headers: ```python from urllib.parse import urlparse OFFICIAL_API_ORIGIN = "https://api.capacities.io" def validate_api_base_url(value: str) -> str: parsed = urlparse(value) if ( parsed.scheme != "https" or parsed.hostname != "api.capacities.io" or parsed.port not in (None, 443) or parsed.username is not None or parsed.password is not None or parsed.params or parsed.query or parsed.fragment ): raise CapacitiesConfigError( "apiBaseUrl must use the trusted Capacities HTTPS origin" ) return OFFICIAL_API_ORIGIN ```
Vulnerability Patterns
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (5)

Tainted flow: 'req' from os.environ.get (line 122, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
for attempt in range(retries + 1):
        req = urllib.request.Request(url, data=body_bytes, headers=headers, method=method.upper())
        try:
            with urllib.request.urlopen(req, timeout=timeout_seconds) as resp:
                raw = resp.read().decode("utf-8", errors="replace")
                if not raw.strip():
                    return {}
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
This finding highlights that the implemented/documented commands appear to focus on syncing structures, verifying spaces, and persisting cache state rather than strictly returning object matches and deep links as advertised. Such description-behavior drift can hide stateful or networked operations behind a seemingly harmless search skill, which raises the risk of over-privileged execution and weak user consent.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
This finding highlights that the implemented/documented commands appear to focus on syncing structures, verifying spaces, and persisting cache state rather than strictly returning object matches and deep links as advertised. Such description-behavior drift can hide stateful or networked operations behind a seemingly harmless search skill, which raises the risk of over-privileged execution and weak user consent.

Lp3

Medium
Category
MCP Least Privilege
Confidence
87% confidence
Finding
The skill declares shell commands that source environment variables, invoke Python, read local config files, write cache data under a workspace directory, and perform live API calls, but it does not declare any explicit tool scope or permissions boundary. That omission can cause an agent runtime or reviewer to underestimate what the skill can access, increasing the chance of unintended secret exposure, filesystem modification, or network use.

Missing User Warnings

Low
Confidence
84% confidence
Finding
The function atomically writes JSON data to workspace files using a temporary file and os.replace, affecting persistent user or system data. Within this file there is no confirmation prompt, logging/print statement, or explanatory docstring/comment disclosing that cache and state files will be created or overwritten.

Static analysis

No suspicious patterns detected.