Back to skill

Security audit

IDFM Journey (PRIM/Navitia)

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly a coherent Île-de-France transit helper, but one command option can send the user's IDFM API key to an arbitrary server.

Review this skill before installing. Use it only in an environment where the IDFM_PRIM_API_KEY is limited in scope, and do not pass --base-url unless you have audited and trust the destination. A safer version should remove that option or restrict it to the official prim.iledefrance-mobilites.fr Navitia endpoint before sending the API key.

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/idfm.py:251
Finding
Caller-Controlled Base URL Can Exfiltrate the IDFM API Key<![CDATA[ ## Vulnerability Details **File Location**: `scripts/idfm.py`, lines 23–50 and 251–272 **Vulnerability Type**: Arbitrary credential-bearing network destination **Risk Level**: High ### Vulnerable Code ```python def _http_get_json(url: str, api_key: str, timeout_s: int = 20) -> dict: req = urllib.request.Request(url) req.add_header("apikey", api_key) req.add_header("accept", "application/json") try: with urllib.request.urlopen(req, timeout=timeout_s) as resp: raw = resp.read().decode("utf-8") return json.loads(raw) except urllib.error.HTTPError as e: body = None try: body = e.read().decode("utf-8", errors="replace") except Exception: pass raise PrimError(f"HTTP {e.code} {e.reason}: {body or ''}".strip()) from e except urllib.error.URLError as e: raise PrimError(f"Network error: {e}") from e class PrimClient: def __init__(self, api_key: str | None = None, base_url: str = BASE_URL): self.api_key = api_key or os.environ.get("IDFM_PRIM_API_KEY") if not self.api_key: raise PrimError("Missing IDFM_PRIM_API_KEY env var") self.base_url = base_url.rstrip("/") def get(self, path: str, params: dict | None = None) -> dict: base = f"{self.base_url}/{path.lstrip('/')}" qs = urllib.parse.urlencode(params or {}, doseq=True) url = f"{base}?{qs}" if qs else base return _http_get_json(url, self.api_key) ``` ```python p.add_argument( "--base-url", default=BASE_URL, help="override PRIM base URL (default: %(default)s)", ) sp = p.add_subparsers(dest="cmd", required=True) p_places = sp.add_parser("places", help="resolve places via /places") p_places.add_argument("query") p_places.add_argument("--count", type=int, default=5) p_j = sp.add_parser("journeys", help="plan a journey via /journeys") p_j.add_argument("--from", dest="from_query", required=True) p_j.add_ar ...[truncated 2907 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the `--base-url` option from the production command-line interface and always use the fixed `BASE_URL` constant. 2. If destination configurability is required for testing, keep it outside the production CLI or require an explicit development-only configuration. 3. Strictly validate any configurable destination before creating a request: - Require the `https` scheme. - Require the exact hostname `prim.iledefrance-mobilites.fr`. - Reject embedded credentials, fragments, unexpected ports, and deceptive hostname suffixes. - Restrict the path to `/marketplace/v2/navitia`. 4. Ensure the API key is only attached after the final request destination has passed validation. 5. Prevent credential forwarding across redirects to a different scheme, hostname, or port. Either disable redirects or validate every redirect target before resending the authentication header. 6. Add automated tests confirming that HTTP URLs, attacker-controlled domains, localhost, private-network addresses, and cross-host redirects are rejected before any credential-bearing request is sent. 7. Rotate the API key if the affected option has previously been used with an untrusted or unintended destination. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (3)

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill instructs the user to run a bundled Python script that reads an environment variable and makes external network requests, but the manifest declares no explicit tool scope or permissions. This creates an authorization and transparency gap: an agent or platform may not clearly signal that the skill needs environment-secret access and outbound network capability, increasing the risk of unintended secret exposure or unauthorized external calls.

Context-Inappropriate Capability

Medium
Confidence
93% confidence
Finding
The CLI exposes a user-controlled --base-url that overrides the expected IDFM/Navitia endpoint, which expands the script from a fixed transit-query tool into a generic authenticated HTTP client. In agent or automation contexts, this can enable SSRF-like behavior, unintended outbound requests, and possible leakage of the IDFM API key to attacker-controlled hosts because the apikey header is sent to whatever URL is provided.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
Several user-facing strings in journey section rendering are fixed in French, such as "attente" and "marche", and this continues in later printed output. The file does not offer a language choice or clearly document that the tool is intentionally French-only, which can violate locale policy requirements.

Static analysis

No suspicious patterns detected.