Back to skill

Security audit

Stormglass Surf & Ocean Data

Security checks for vulnerabilities and agentic risk

Overview

Review recommended: the skill matches its surf-report purpose, but it can expose a Google geocoding API key in error logs.

Install only if you are comfortable with location queries being sent to Stormglass and a geocoder. If using GOOGLE_GEOCODING_API_KEY, restrict the key in Google Cloud, avoid persistent stderr logging until URL redaction is fixed, and rotate the key if logs may already contain key= values.

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/surf_report.py:93
Finding

Google Geocoding API Key Disclosure Through Error Logging

Content
View full analysis

Vulnerability Details

File Location: scripts/surf_report.py:93-104, scripts/surf_report.py:146-147, and examples.md:71-74
Vulnerability Type: Sensitive credential exposure through unredacted URLs
Risk Level: Medium

The Google Geocoding API key is included in a URL query parameter. When an HTTP error or invalid JSON response occurs, the complete URL is incorporated into an exception message. The main program subsequently writes that message to stderr, and the documented cron configuration appends stderr to a persistent log file.

Vulnerable Code

scripts/surf_report.py:93-104:

python
try:
    with urllib.request.urlopen(req, timeout=timeout) as response:
        body = response.read().decode("utf-8")
except urllib.error.HTTPError as exc:
    detail = exc.read().decode("utf-8", errors="replace")
    raise ApiError(f"HTTP {exc.code} for {url}: {detail}") from exc
except urllib.error.URLError as exc:
    raise ApiError(f"Network error for {url}: {exc}") from exc

try:
    parsed = json.loads(body)
except json.JSONDecodeError as exc:
    raise ApiError(f"Invalid JSON from {url}: {exc}") from exc

scripts/surf_report.py:146-147:

python
query = urllib.parse.urlencode({"address": address, "key": api_key})
payload = http_get_json(f"{GEOCODE_ENDPOINT}?{query}", headers=None, timeout=timeout)

The exception is ultimately written to stderr:

python
except ApiError as exc:
    print(f"ERROR: {exc}", file=sys.stderr)
    return 4

examples.md:71-74 recommends persisting that stderr output:

cron
*/30 * * * * cd /home/dgorissen/code/stormglass-skill && python scripts/surf_report.py --location "Highcliffe Beach" --horizon 72h --output json >> logs/surf_report.jsonl 2>> logs/surf_report.err

Technical Analysis

Credentials in URL query strings must be treated as sensitive. The url passed to http_get_json contains both ...[truncated 2185 chars]

Remediation
View remediation

Remediation Suggestions

  1. Never include a complete request URL in an exception when it may contain credentials. Log only the endpoint scheme, host, and path.

  2. Add centralized URL sanitization that removes sensitive parameters such as key, token, api_key, and access_token before any diagnostic output:

    python
    def redact_url(url: str) -> str:
        parsed = urllib.parse.urlsplit(url)
        query = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
        safe_query = [
            (name, "[REDACTED]" if name.lower() in {"key", "token", "api_key", "access_token"} else value)
            for name, value in query
        ]
        return urllib.parse.urlunsplit(
            (
                parsed.scheme,
                parsed.netloc,
                parsed.path,
                urllib.parse.urlencode(safe_query),
                parsed.fragment,
            )
        )
    

    Use only the sanitized value in all error branches:

    python
    safe_url = redact_url(url)
    raise ApiError(f"HTTP {exc.code} for {safe_url}") from exc
    
  3. Avoid including arbitrary upstream response bodies in routine error logs because they may contain request details or other sensitive information. Limit and sanitize any diagnostic body content.

  4. Configure the Google key with API restrictions, application restrictions where practical, quota limits, and billing alerts.

  5. Restrict log-file permissions, implement short retention and secure rotation, and prevent credential-bearing logs from entering broadly accessible aggregation systems.

  6. Search existing logs and backups for exposed key= parameters. Rotate the Google key if exposure may already have occurred.

  7. Add automated tests that simulate HTTP and malformed-JSON failures and assert that neither API keys nor unredacted credential parameters appear in stderr.

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • 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 (8)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding

The skill explicitly requires environment variables and external binaries and describes live API usage against Google, OpenStreetMap, and Stormglass, but it does not declare any explicit tool scope such as allowed-tools or permissions. That creates an authorization gap where an agent runtime may grant broader shell, network, or env access than is minimally necessary, increasing the risk of unintended command execution, unrestricted outbound requests, or secret exposure through the skill.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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 · reference.md (reported line 6)May include surrounding context.

md
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple

WEATHER_ENDPOINT = "https://api.stormglass.io/v2/weather/point"
TIDE_ENDPOINTS = [
    "https://api.stormglass.io/v2/tide/extremes/point",
    "https://api.stormglass.io/v2/tide/extremes",

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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 · reference.md (reported line 7)May include surrounding context.

md
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple

WEATHER_ENDPOINT = "https://api.stormglass.io/v2/weather/point"
TIDE_ENDPOINTS = [
    "https://api.stormglass.io/v2/tide/extremes/point",
    "https://api.stormglass.io/v2/tide/extremes",

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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 · reference.md (reported line 8)May include surrounding context.

md
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple

WEATHER_ENDPOINT = "https://api.stormglass.io/v2/weather/point"
TIDE_ENDPOINTS = [
    "https://api.stormglass.io/v2/tide/extremes/point",
    "https://api.stormglass.io/v2/tide/extremes",

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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 · scripts/surf_report.py (reported line 40)May include surrounding context.

python
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple

WEATHER_ENDPOINT = "https://api.stormglass.io/v2/weather/point"
TIDE_ENDPOINTS = [
    "https://api.stormglass.io/v2/tide/extremes/point",
    "https://api.stormglass.io/v2/tide/extremes",

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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 · scripts/surf_report.py (reported line 42)May include surrounding context.

python
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple

WEATHER_ENDPOINT = "https://api.stormglass.io/v2/weather/point"
TIDE_ENDPOINTS = [
    "https://api.stormglass.io/v2/tide/extremes/point",
    "https://api.stormglass.io/v2/tide/extremes",

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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 · scripts/surf_report.py (reported line 43)May include surrounding context.

python
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple

WEATHER_ENDPOINT = "https://api.stormglass.io/v2/weather/point"
TIDE_ENDPOINTS = [
    "https://api.stormglass.io/v2/tide/extremes/point",
    "https://api.stormglass.io/v2/tide/extremes",

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/test_surf_report.py (reported line 30)May include surrounding context.

python
def run_case(args: List[str]) -> subprocess.CompletedProcess[str]:
    cmd = [PYTHON_BIN, str(CLI)] + args
    return subprocess.run(cmd, capture_output=True, text=True, check=False, cwd=str(ROOT))


def assert_json_shape(stdout: str) -> None:

Static analysis

No suspicious patterns detected.