Back to skill

Security audit

air-train-ev

Security checks for vulnerabilities and agentic risk

Overview

The skill provides legitimate travel lookup features, but its scripts can send API credentials and travel or location queries to arbitrary configured hosts without validation.

Review before installing. Use only official HTTPS API hosts, avoid setting the *_HOST overrides unless you fully control the endpoint and credentials, and treat EV coordinates and flight/transit searches as externally transmitted data. Avoid --dump for sensitive trips unless you intend to store the raw response locally; rotate keys if errors or logs may have exposed query URLs.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/navitia.py:34
Finding
Navitia credentials can be redirected to an arbitrary or plaintext host<![CDATA[ ## Vulnerability Details **File Location**: `scripts/navitia.py`, lines 34-49 **Vulnerability Type**: Unrestricted credential destination and missing transport validation **Risk Level**: High ### Vulnerable Code ```python def navitia_get(path: str, query: dict | None = None) -> dict: host = os.environ.get("NAVITIA_HOST", "https://api.navitia.io").rstrip("/") token = env("NAVITIA_TOKEN") qs = f"?{urllib.parse.urlencode(query)}" if query else "" url = f"{host}{path}{qs}" req = urllib.request.Request(url, method="GET") # Navitia accepts token as basic auth username, but the docs also show Authorization header usage. # We use Basic auth with token as username and empty password. auth = base64.b64encode(f"{token}:".encode("utf-8")).decode("ascii") req.add_header("Authorization", f"Basic {auth}") try: with urllib.request.urlopen(req, timeout=30) as resp: ``` ### Technical Analysis The Navitia token is Base64-encoded and placed in an HTTP Basic Authorization header. This encoding is required by the Basic authentication protocol and is not encryption. The encoded value is not directly printed to stdout, so the Base64 operation is not itself a covert output channel. The security issue is that `NAVITIA_HOST` is accepted without validating its scheme, hostname, port, or trust level. Consequently, the credential-bearing request may be sent to an arbitrary endpoint, including a plaintext `http://` endpoint. The HTTP client may also follow redirects, creating another opportunity for unintended credential disclosure depending on redirect handling. External network access and transmission of the token to Navitia are necessary for the declared transit-planning functionality. Allowing the token to be sent to any environment-selected destination exceeds the minimum privilege required. ### Attack Path 1. An attacker or compromised runtime configuration sets `NAVITIA_HOST` to an attacker-controlled URL, such as `h ...[truncated 937 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Parse the configured host using `urllib.parse.urlsplit`. - Require the `https` scheme for all credential-bearing requests. - Allowlist `api.navitia.io` in production. - If custom hosts are needed for testing, require a separate explicit development option and document that credentials must not be production credentials. - Reject URLs containing user information, fragments, unexpected ports, or malformed hostnames. - Prevent credential-bearing requests from following redirects to a different origin. - Continue keeping the token out of exception messages and normal output. - Consider centralizing URL validation so every API client applies the same policy. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/flight_offers.py:66
Finding
Amadeus client credentials and access tokens can be sent to an untrusted host<![CDATA[ ## Vulnerability Details **File Location**: `scripts/flight_offers.py`, lines 66-75 and 158-184 **Vulnerability Type**: Unrestricted OAuth credential destination and missing transport validation **Risk Level**: High ### Vulnerable Code ```python def get_access_token(host: str) -> str: client_id = env("AMADEUS_CLIENT_ID") client_secret = env("AMADEUS_CLIENT_SECRET") token_url = f"{host}/v1/security/oauth2/token" j = post_form( token_url, { "grant_type": "client_credentials", "client_id": client_id, "client_secret": client_secret, }, ) token = j.get("access_token") ``` ```python def main() -> int: args = parse_args() host = os.environ.get("AMADEUS_HOST", "https://api.amadeus.com").rstrip("/") token = get_access_token(host) travel_class = (args.travel_class or "").strip().upper() travel_class_map = { "ECO": "ECONOMY", "ECONOMY": "ECONOMY", "PREMIUM_ECO": "PREMIUM_ECONOMY", "PREMIUM_ECONOMY": "PREMIUM_ECONOMY", "BUSINESS": "BUSINESS", "FIRST": "FIRST", } if travel_class not in travel_class_map: raise RuntimeError(f"Unsupported travel class: {args.travel_class} (try ECONOMY, PREMIUM_ECONOMY, BUSINESS, FIRST)") params = { "originLocationCode": args.origin, "destinationLocationCode": args.destination, "departureDate": args.departure, "adults": str(args.adults), "travelClass": travel_class_map[travel_class], "nonStop": args.non_stop, "max": str(args.max), } if args.return_date: params["returnDate"] = args.return_date if args.included_airlines.strip(): params["includedAirlineCodes"] = args.included_airlines.strip() url = f"{host}/v2/shopping/flight-offers?{urllib.parse.urlencode(params)}" j = get_json(url, headers={"Authorization": f"Bearer {token}"}) ``` ### Technical Analysis The value of `A ...[truncated 2067 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Require `AMADEUS_HOST` to use HTTPS. - Allowlist the intended Amadeus production and approved test domains. - Treat custom endpoints as a development-only feature requiring explicit opt-in. - Validate scheme, normalized hostname, port, user information, and fragments before using the URL. - Reject cross-origin redirects for OAuth and Bearer-token requests. - Consider separating and independently validating the token and API endpoints. - Use non-production credentials when connecting to test endpoints. - Avoid including credential-bearing request bodies or Authorization values in diagnostics. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/ev_charge_points.py:106
Finding
Open Charge Map API key and precise coordinates can be redirected to an arbitrary host<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ev_charge_points.py`, lines 106-138 **Vulnerability Type**: Unrestricted sensitive-data destination and missing transport validation **Risk Level**: High ### Vulnerable Code ```python def main() -> int: args = parse_args() host = os.environ.get("OPENCHARGEMAP_HOST", "https://api.openchargemap.io").rstrip("/") key = env("OPENCHARGEMAP_API_KEY") compact = "false" if args.verbose else "true" verbose = "true" if args.verbose else "false" params = { "output": "json", "latitude": f"{args.lat}", "longitude": f"{args.lon}", "distance": f"{args.km}", "distanceunit": "KM", "maxresults": str(args.max), "compact": compact, "verbose": verbose, "key": key, } if args.countrycode: params["countrycode"] = args.countrycode if args.operators: params["operatorid"] = args.operators if args.usage: params["usagetypeid"] = args.usage url = f"{host}/v3/poi/?{urllib.parse.urlencode(params)}" data = get_json(url) ``` ### Technical Analysis The script places the Open Charge Map key and user-supplied latitude and longitude in a query URL whose origin is controlled by `OPENCHARGEMAP_HOST`. The host is not checked for HTTPS or restricted to the documented service domain. Transmission of coordinates to Open Charge Map is inherent to nearby charging-station search. However, unrestricted destination selection permits both the API key and potentially sensitive location information to be sent to an attacker-controlled server. Plain HTTP configuration would also expose this information to passive network observers. ### Attack Path 1. An attacker or compromised runtime sets `OPENCHARGEMAP_HOST` to a server under attacker control. 2. The user invokes the charging-point search with latitude and longitude. 3. The script adds the API key and coordinates to the request query. 4. The request ...[truncated 603 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Require an HTTPS URL with the normalized hostname `api.openchargemap.io`. - Reject untrusted schemes, user information, fragments, unexpected ports, and malformed hostnames. - Make support for custom hosts an explicit development-only option. - Prevent requests carrying keys or coordinates from being redirected to a different origin. - Where supported by the API, send credentials in an authorization header rather than the query string. - Document that latitude and longitude are transmitted externally and should be limited to the precision required by the search. - Consider rounding coordinates when exact precision is not necessary. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/ev_charge_points.py:41
Finding
Open Charge Map API key is disclosed in HTTP error output<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ev_charge_points.py`, lines 41-43, 119-128, and 148-152 **Vulnerability Type**: Sensitive credential exposure through URLs and exception logging **Risk Level**: Medium ### Vulnerable Code ```python try: with urllib.request.urlopen(req, timeout=30) as resp: raw = resp.read().decode("utf-8") except urllib.error.HTTPError as e: txt = e.read().decode("utf-8", errors="replace") raise RuntimeError(f"HTTP {e.code} for {url}: {txt}") from e ``` ```python params = { "output": "json", "latitude": f"{args.lat}", "longitude": f"{args.lon}", "distance": f"{args.km}", "distanceunit": "KM", "maxresults": str(args.max), "compact": compact, "verbose": verbose, "key": key, } url = f"{host}/v3/poi/?{urllib.parse.urlencode(params)}" ``` ```python if __name__ == "__main__": try: raise SystemExit(main()) except Exception as e: print(f"ERROR: {e}", file=sys.stderr) raise ``` ### Technical Analysis The API key is embedded in the query string as `key=<secret>`. When an HTTP error occurs, `get_json` includes the complete URL in a `RuntimeError`. The top-level exception handler then prints that exception to stderr and re-raises it. As a result, the key and exact coordinates can appear in Agent output, terminal history, process diagnostics, centralized logs, monitoring services, or retained error traces. Query-string credentials may also be recorded by proxies and web servers even when the request succeeds. ### Attack Path 1. The user invokes the EV charging-point search. 2. The script constructs a URL containing the API key and coordinates. 3. The remote API, proxy, or attacker-controlled endpoint returns an HTTP error. 4. The exception message incorporates the complete URL. 5. The top-level handler writes the message to stderr and re-raises the exception. 6. ...[truncated 519 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Never include an unsanitized credential-bearing URL in an exception. - Redact sensitive parameters before logging, for example by replacing `key` with `[REDACTED]`. - Prefer an authorization header if Open Charge Map supports one. - If the API requires a query parameter, maintain separate request and display URLs. - Ensure exception chains and debug logs do not retain the unredacted URL. - Apply structured logging with an explicit allowlist of safe fields. - Review proxy, server, Agent, and observability logs for previously exposed keys and rotate the key if exposure may have occurred. - Redact or reduce coordinate precision in diagnostics. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (5)

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

Critical
Category
Data Flow
Content
req.add_header("Authorization", f"Basic {auth}")

    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            raw = resp.read().decode("utf-8")
    except urllib.error.HTTPError as e:
        txt = e.read().decode("utf-8", errors="replace")
Confidence
95% confidence
Finding
The request destination is derived from the NAVITIA_HOST environment variable and is used directly to construct the outbound URL while also attaching the NAVITIA_TOKEN in an Authorization header. If an attacker can influence the environment, they can redirect the client to an arbitrary host and exfiltrate the API token or force requests to internal services, making this a real SSRF/credential-leak risk.

Vague Triggers

Medium
Confidence
92% confidence
Finding
The skill description says to use the skill when 'Alessandro asks' for broad categories of travel and mobility tasks, which creates a person-specific trigger with unclear boundaries. This can cause the agent to invoke external API-backed functionality too eagerly for a particular user, increasing the chance of unintended data sharing, unnecessary API calls, or inappropriate tool use outside explicit user intent.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
The markdown states that output formatting is fixed to `DD/MM/YY HH:MM` and that EUR prices use `€`, which imposes a specific locale/formatting convention. Because no user opt-in or region-specific justification is provided, this is a natural-language locale policy concern.

External Transmission

Medium
Category
Data Exfiltration
Content
"""Find nearby EV charge points via Open Charge Map.

Docs: https://openchargemap.org/site/develop/api
API: GET https://api.openchargemap.io/v3/poi/

Env:
- OPENCHARGEMAP_API_KEY (required)
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Low
Confidence
82% confidence
Finding
The --dump option writes the full JSON response to disk, which is a file write operation. While the argument name suggests output, there is no confirmation prompt, warning print, or explanatory comment/docstring near the write to disclose that potentially sensitive travel data will be persisted locally.

Static analysis

No suspicious patterns detected.