Back to skill

Security audit

Linz Public Transport

Security checks for vulnerabilities and agentic risk

Overview

The skill appears to perform Linz transit lookups, but it allows arbitrary API destinations and defaults to unencrypted HTTP, so it should be reviewed before installation.

Install only if you are comfortable with the skill making outbound transit API requests and can constrain it to the official Linz HTTPS endpoint. Prefer a version that defaults to HTTPS, rejects arbitrary base URLs in normal use, and avoids logging full query URLs containing stop searches.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/linz_transport.py:23
Finding

Unrestricted API Base URL Enables Server-Side Request Forgery

Content
View full analysis
str: base = base_url.rstrip("/") return f"{base}/{path.lstrip('/')}" def http_get_json(url: str, timeout: int) -> Any: req = urllib.request.Request(url=url, method="GET") try: with urllib.request.urlopen(req, timeout=timeout) as resp: body = resp.read().decode("utf-8") return json.loads(body) except urllib.error.HTTPError as err: raise RuntimeError(f"HTTP {err.code} for {url}") from err except urllib.error.URLError as err: raise RuntimeError(f"Network error for {url}: {err.reason}") from err except json.JSONDecodeError as err: raise RuntimeError(f"Invalid JSON from {url}: {err}") from err def query_efa(base_url: str, endpoint: str, params: dict[str, Any], timeout: int) -> dict[str, Any]: query = urllib.parse.urlencode({k: v for k, v in params.items() if v is not None}) url = build_url(base_url, f"/efa/{endpoint}?{query}") payload = http_get_json(url, timeout) if not isinstance(payload, dict): raise RuntimeError(f"Expected JSON object from {endpoint}") return payload ``` ```python parser.add_argument( "--base-url", default=os.environ.get("LINZ_TRANSPORT_API_BASE_URL", DEFAULT_BASE_URL), help=( "API base URL. Defaults to LINZ_TRANSPORT_API_BASE_URL or " f"{DEFAULT_BASE_URL}." ), ) ``` The corresponding behavior is explicitly documented in `SKILL.md:26-29, 35-38`, which permits the base URL to be supplied through either a command-line argument or an environment variable. ### Technical Analysis The script accepts a caller-controlled base URL and passes the resulting URL directly to `urllib.request.urlopen`. It does not ...[truncated 2338 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/linz_transport.py:20
Finding

Transit Search Data Is Transmitted over Plaintext HTTP

Content
View full analysis
list[dict[str, Any]]: payload = query_efa( base_url=base_url, endpoint="XML_STOPFINDER_REQUEST", params={ "locationServerActive": 1, "outputFormat": "JSON", "type_sf": "any", "name_sf": name, }, timeout=timeout, ) ``` ```python def get_departures(base_url: str, stop_id: str, limit: int, timeout: int) -> list[dict[str, Any]]: payload = query_efa( base_url=base_url, endpoint="XML_DM_REQUEST", params={ "locationServerActive": 1, "stateless": 1, "outputFormat": "JSON", "type_dm": "any", "name_dm": stop_id, "mode": "direct", "limit": limit, }, timeout=timeout, ) ``` The insecure default is also declared in `SKILL.md:4, 26-29, 35-38`. In contrast, `references/endpoints.md:5` records successful endpoint probes using `https://www.linzag.at/linz2`, indicating that HTTPS is available. ### Technical Analysis The default API origin uses HTTP rather than HTTPS. Stop names and stop IDs are placed in URL query parameters and transmitted to this endpoint. Stop queries can reveal a user's intended destination, travel activity, or approximate location. Because no TLS protection is present, any network intermediary capable of observing the connection can inspect the complete request URL and transit query. An active intermediary can also modify the API response before it is parsed, allowing departure times, destinations, line information, and stop matches to be manipulated. Network a ...[truncated 1251 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Note
Location
scripts/linz_transport.py:28
Finding

Full Request URLs Expose Stop Queries in Error Logs

Content
View full analysis
Any: req = urllib.request.Request(url=url, method="GET") try: with urllib.request.urlopen(req, timeout=timeout) as resp: body = resp.read().decode("utf-8") return json.loads(body) except urllib.error.HTTPError as err: raise RuntimeError(f"HTTP {err.code} for {url}") from err except urllib.error.URLError as err: raise RuntimeError(f"Network error for {url}: {err.reason}") from err except json.JSONDecodeError as err: raise RuntimeError(f"Invalid JSON from {url}: {err}") from err ``` ```python def main(argv: list[str]) -> int: try: args = parse_args(argv) result = args.handler(args) print(json.dumps(result, ensure_ascii=True, indent=2)) return 0 except RuntimeError as err: print(f"Error: {err}", file=sys.stderr) return 2 ``` The URL passed to `http_get_json` contains either `name_sf=` or `name_dm=`. On an HTTP failure, network failure, or JSON parsing failure, the complete URL is inserted into a `RuntimeError` and printed to stderr. ### Technical Analysis The implementation logs the complete request URL instead of a sanitized endpoint identifier. Since user-provided stop names and stop IDs are encoded into the query string, failures cause these values to be written to stderr. In Agent and automation environments, stderr is commonly captured in command transcripts, centralized logs, telemetry, or debugging records. This creates unnecessary secondary storage of potentially location-related information and can extend its retention beyond the transit query itself. ### Attack Path 1. A user submits a stop name or stop ID. 2. Th ...[truncated 889 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (4)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill requires network access and reads an environment variable for its base URL, but it does not explicitly declare a restrictive tool scope such as permissions or allowed-tools. That creates an authorization gap where the runtime may grant broader capabilities than reviewers or policy expect, and the env-provided base URL can direct requests to arbitrary hosts if not tightly constrained.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · references/endpoints.md (reported line 4)May include surrounding context.

md
# Endpoint Reference

Sources:
- EFA XML API PDF: `https://data.linz.gv.at/katalog/linz_ag/linz_ag_linien/fahrplan/EFA_XML_Schnittstelle_20151217.pdf`
- Live endpoint probes against `https://www.linzag.at/linz2` on February 13, 2026
- Open data catalog RDF: `https://www.data.gv.at/api/hub/repo/datasets/linien-fahrwege-und-haltestellen-der-linz-ag-linien-2025.rdf?useNormalizedId=true&locale=de`

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The default base URL uses plain HTTP, so queried stop names and stop IDs are transmitted without transport encryption and responses are unauthenticated. This allows network attackers to observe or modify requests and responses, which is especially problematic in an agent/tooling context because the agent may trust manipulated transit data or disclose user-provided queries over insecure networks.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The skill allows the API base URL to be overridden via a command-line flag or environment variable, so an operator or calling agent can direct requests to arbitrary hosts unrelated to Linz transport. In an agent setting this broadens the tool from a narrowly scoped public-transport lookup into a generic outbound HTTP client, which can enable policy bypass, unintended data disclosure to third parties, or access to internal services if the runtime environment has network reachability.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.