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

Content
View full analysis

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
View 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
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/capacities_client.py (reported line 124)May include surrounding context.

python
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 {}

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.

Content

No source excerpt is available for this finding.

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.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.