Back to skill

Security audit

NS Trains

Security checks for vulnerabilities and agentic risk

Overview

This skill is mostly coherent for checking Dutch train information, but its NS API key handling has a real redirect-related exposure risk that users should review before installing.

Install only if you are comfortable providing an NS subscription key to these scripts. Prefer a low-privilege/free-tier key, inject it through your runtime secret mechanism, monitor or rotate it if exposed, and be aware that the current redirect handling should be tightened before using a valuable or sensitive API subscription.

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

Warning
Location
scripts/ns-api.mjs:28
Finding
NS Subscription Key May Be Disclosed Across HTTP Redirects## Vulnerability Details **File Location**: `scripts/ns-api.mjs`, lines 28–39 **Vulnerability Type**: Credential exposure through unvalidated redirects **Risk Level**: Medium ```js export async function nsFetch(url, { subscriptionKey, headers = {}, ...opts } = {}) { const u = assertAllowlistedUrl(url); const res = await fetch(u, { ...opts, headers: { 'Ocp-Apim-Subscription-Key': subscriptionKey, 'Accept': 'application/json', ...headers, }, }); return res; } ``` ### Technical Analysis `assertAllowlistedUrl()` validates only the initial request URL. It requires HTTPS and restricts the hostname to `gateway.apiportal.ns.nl`. However, `fetch()` follows HTTP redirects by default, and the redirect destination is not passed through the URL validation function. The request includes the sensitive `Ocp-Apim-Subscription-Key` custom header. Unlike certain standard credential headers that implementations may remove during cross-origin redirects, this custom header cannot safely be assumed to be stripped. Therefore, a redirect to another origin may cause the subscription key to be transmitted outside the documented host allowlist. This behavior also makes the guarantee in `SECURITY.md`—that only the NS API gateway receives requests—stronger than the protection actually enforced by the implementation. ### Attack Path 1. A legitimate script invokes `nsFetch()` with the user's NS subscription key. 2. The allowlisted NS gateway, or an endpoint under that gateway, returns an HTTP redirect. 3. Exploitation requires the trusted gateway or its response path to be compromised, maliciously configured, or otherwise capable of redirecting to an attacker-controlled HTTPS origin. 4. Because redirect handling remains at the default `follow` setting, `fetch()` follows the redirect without invoking `assertAllowlistedUrl()` again. 5. The custom `Ocp-Apim-Subscription-Key` header may be forwarde ...[truncated 647 chars]
Remediation
## Remediation Suggestions - Set `redirect: 'manual'` in the initial `fetch()` request and reject redirects unless they are explicitly required. - If redirects must be supported, resolve each `Location` header against the current URL and validate every destination with the same HTTPS and exact-host allowlist before making another request. - Never forward `Ocp-Apim-Subscription-Key` when the redirect changes the origin. - Impose a small redirect limit to prevent loops and redirect-based resource exhaustion. - Prevent callers from overriding the redirect policy through `opts`; apply the security-controlled option after spreading caller options. - Add automated tests covering redirects to non-allowlisted hosts, non-HTTPS destinations, malformed locations, redirect loops, and same-host redirects. - Update `SECURITY.md` so its egress guarantee precisely reflects the implemented redirect policy.
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (11)

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill requires environment access for an API key and makes outbound network requests, but it does not declare an explicit tool scope such as permissions or allowed-tools. That creates unnecessary ambient authority: a host agent may grant broader access than needed, making misuse of secrets or network capabilities more likely if the skill is invoked in an unexpected context.

Vague Triggers

Medium
Confidence
94% confidence
Finding
The natural-language examples are broad and generic, such as asking about trains or commute planning, without defining clear invocation boundaries or confirmation requirements. This can cause over-triggering or unintended skill activation, potentially exposing commute-related data or consuming API quota when a user did not explicitly intend to run this skill.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The script hard-codes the 'nl-NL' locale in user-facing time formatting, which enforces a specific language/locale choice regardless of user preference. This is a natural-language policy issue because the file provides no opt-in, fallback, or justification that the tool is region-specific.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The updated timestamp uses toLocaleTimeString('nl-NL'), again forcing a specific locale in output. Because the file does not offer a language/locale choice or explain that Dutch-only output is required, this violates the stated policy criteria.

Natural-Language Policy Violations

Low
Confidence
92% confidence
Finding
The script hard-codes the 'nl-NL' locale in time formatting calls, which forces a specific language/locale presentation for output. This is a natural-language policy concern because users are not offered a locale choice and the file does not document why Dutch formatting is required.

Natural-Language Policy Violations

Low
Confidence
90% confidence
Finding
The final timestamp output also hard-codes 'nl-NL', reinforcing a fixed locale choice throughout the user-facing output. Without an opt-in or explicit regional justification, this violates the language/locale policy criteria.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The code hardcodes the 'nl-NL' locale in toLocaleTimeString, which forces output formatting for all users regardless of their preferred language or locale. This is a natural-language/locale policy issue because the skill does not offer a choice or explain why Dutch locale is required.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The update timestamp is formatted with the fixed 'nl-NL' locale, which imposes a specific locale on all users. The file provides no user choice or explanation that this tool is intentionally limited to Dutch locale output.

Natural-Language Policy Violations

Low
Confidence
94% confidence
Finding
The call to toLocaleTimeString('nl-NL') hard-codes a specific locale in user-facing output. This is a natural-language/locale policy issue because the script does not offer a user opt-in or configuration choice, and the file itself does not document a justified region-specific constraint.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The code hard-codes 'nl-NL' in time formatting for displayed departure and arrival times. This imposes a specific locale on all users, and the file does not offer an opt-in, fallback, or explanation that this locale restriction is required.

Natural-Language Policy Violations

Low
Confidence
94% confidence
Finding
The final 'Checked' timestamp is rendered with toLocaleTimeString('nl-NL'), which enforces a specific language/locale policy in output. There is no visible user opt-in or documentation indicating that Dutch-only formatting is required for this skill.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/ns-api.mjs:10