Back to skill

Security audit

Skills of HK-share data released by ft.tech.

Security checks for vulnerabilities and agentic risk

Overview

This skill is a coherent Hong Kong stock-data lookup tool that runs local handlers to fetch public market data from a disclosed HTTPS API, with no evidence of credential access, persistence, or destructive behavior.

Install only if you are comfortable with the agent running the included Python scripts and making unauthenticated HTTPS requests to market.ft.tech for Hong Kong stock data. The publisher should consider adding explicit permission metadata, request timeouts, response-size limits, and stricter redirect handling.

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

T09 · Insecure Skill Coding Practices

Warning
Location
sub-skills/company-hk/scripts/handler.py:9
Finding

Redirect destinations bypass the outbound URL allowlist

Content
View full analysis

Vulnerability Details

File Locations:

  • sub-skills/company-hk/scripts/handler.py:9-22, 39-42
  • sub-skills/hk-candlesticks/scripts/handler.py:11-35, 124-127
  • sub-skills/hk-valuatnanalyd/scripts/handler.py:9-22, 55-58
  • sub-skills/hk-view/scripts/handler.py:9-22, 39-42

Vulnerability Type: Outbound request allowlist bypass through automatic redirects
Risk Level: Medium

Vulnerable Code

The following implementation is present in each handler:

python
SAFE_URLOPENER = urllib.request.build_opener()

BASE_URL = "https://market.ft.tech"


def safe_urlopen(req_or_url):
    if isinstance(req_or_url, urllib.request.Request):
        url = req_or_url.full_url
    else:
        url = str(req_or_url)
    parsed = urllib.parse.urlparse(url)
    if parsed.scheme != "https" or parsed.netloc != "market.ft.tech":
        print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
        sys.exit(1)
    return SAFE_URLOPENER.open(req_or_url)

The validated request is subsequently opened as follows:

python
req = urllib.request.Request(url, method="GET")

try:
    with safe_urlopen(req) as resp:
        data = json.loads(resp.read().decode())
    print(json.dumps(data, ensure_ascii=False, indent=2))

Technical Analysis

safe_urlopen verifies only the initial request URL. It requires the initial scheme to be HTTPS and the initial network location to equal market.ft.tech.

However, urllib.request.build_opener() includes Python's default redirect handling. If the approved endpoint returns an HTTP redirect, the opener can follow that redirect without invoking the validation logic again for the new destination. The redirect target may therefore use a different hostname or, depending on redirect handling, a less secure scheme.

This creates a time-of-check versus time-of-use discrepancy: the URL that passes the allowlist check is not necessarily the final URL from ...[truncated 1612 chars]

Remediation
View remediation

Remediation Suggestions

  1. Disable automatic redirects and reject redirect responses unless redirects are explicitly required.
  2. If redirects are required, implement a custom HTTPRedirectHandler that validates every redirect target before following it.
  3. Require the normalized destination scheme to be exactly https.
  4. Require the normalized hostname to be exactly market.ft.tech.
  5. Reject unexpected ports and user-information components.
  6. Resolve the hostname and block loopback, private, link-local, multicast, and reserved IP address ranges where appropriate.
  7. Apply the same validation after every redirect rather than only to the original request.
  8. Set a small maximum redirect count to prevent redirect loops.

Example hardening approach:

python
class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        raise urllib.error.HTTPError(
            req.full_url, code, "Redirects are not permitted", headers, fp
        )


SAFE_URLOPENER = urllib.request.build_opener(NoRedirectHandler())

If redirects must be supported, replace rejection with strict validation of newurl before constructing the redirected request.

T09 · Insecure Skill Coding Practices

Warning
Location
sub-skills/company-hk/scripts/handler.py:22
Finding

Unbounded network reads and missing request timeouts permit denial of service

Content
View full analysis

Vulnerability Details

File Locations:

  • sub-skills/company-hk/scripts/handler.py:22, 39-42
  • sub-skills/hk-candlesticks/scripts/handler.py:35, 124-127
  • sub-skills/hk-valuatnanalyd/scripts/handler.py:22, 55-58
  • sub-skills/hk-view/scripts/handler.py:22, 39-42

Vulnerability Type: Unbounded remote response consumption and missing network timeout
Risk Level: Medium

Vulnerable Code

Each handler opens the remote request without an explicit timeout:

python
def safe_urlopen(req_or_url):
    if isinstance(req_or_url, urllib.request.Request):
        url = req_or_url.full_url
    else:
        url = str(req_or_url)
    parsed = urllib.parse.urlparse(url)
    if parsed.scheme != "https" or parsed.netloc != "market.ft.tech":
        print(f"Invalid URL for safe_urlopen: {url}", file=sys.stderr)
        sys.exit(1)
    return SAFE_URLOPENER.open(req_or_url)

Each handler then reads the entire response body into memory before decoding and parsing it:

python
req = urllib.request.Request(url, method="GET")

try:
    with safe_urlopen(req) as resp:
        data = json.loads(resp.read().decode())
    print(json.dumps(data, ensure_ascii=False, indent=2))
except urllib.error.HTTPError as e:
    body = e.read().decode()
    print(f"HTTP {e.code}: {body}", file=sys.stderr)
    sys.exit(1)
except urllib.error.URLError as e:
    print(f"Request failed: {e}", file=sys.stderr)
    sys.exit(1)

Error bodies are also read without a size limit:

python
except urllib.error.HTTPError as e:
    body = e.read().decode()
    print(f"HTTP {e.code}: {body}", file=sys.stderr)
    sys.exit(1)

Technical Analysis

SAFE_URLOPENER.open(req_or_url) does not receive an explicit timeout. A slow, stalled, or intentionally non-terminating server can therefore hold the execution path for an excessive period.

In addition, resp.read() and e.read() have no ma ...[truncated 1772 chars]

Remediation
View remediation

Remediation Suggestions

  1. Pass an explicit timeout to every network request, for example:
python
CONNECT_TIMEOUT_SECONDS = 10
return SAFE_URLOPENER.open(
    req_or_url,
    timeout=CONNECT_TIMEOUT_SECONDS,
)
  1. Read responses incrementally and enforce a strict maximum body size:
python
MAX_RESPONSE_BYTES = 5 * 1024 * 1024


def read_limited(resp):
    content_length = resp.headers.get("Content-Length")
    if content_length is not None and int(content_length) > MAX_RESPONSE_BYTES:
        raise ValueError("Response exceeds the permitted size")

    body = resp.read(MAX_RESPONSE_BYTES + 1)
    if len(body) > MAX_RESPONSE_BYTES:
        raise ValueError("Response exceeds the permitted size")
    return body
  1. Apply the same size restriction to HTTP error bodies.
  2. Reject unsupported or unexpectedly large Content-Length values before reading.
  3. Validate response Content-Type before parsing it as JSON.
  4. Catch timeout, Unicode decoding, oversized-response, and json.JSONDecodeError exceptions and terminate cleanly.
  5. Add positive local bounds for page, page_size, limit, and interval_value. For example, require page >= 1, constrain page_size to a documented maximum, require limit > 0, and enforce interval_value == 1.
  6. Where the runtime supports it, enforce separate connection, read, and overall execution deadlines outside the handler as defense in depth.
Vulnerability Patterns
  • 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
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (6)

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 27)May include surrounding context.

md
`run.py` 与本文件(`SKILL.md`)位于同一目录。执行时:

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 69)May include surrounding context.

md
`run.py` 与本文件(`SKILL.md`)位于同一目录。执行时:

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
80% confidence
Finding

The skill documentation instructs the agent to execute a local Python script and states that the skill uses file path resolution and remote HTTPS APIs, but it does not declare any explicit tool scope such as allowed-tools or permissions. That creates an over-privileged, under-specified execution boundary where an agent may invoke shell, read files, and access the network without a policy guardrail, increasing the chance of unintended command execution or data access.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The file’s natural-language documentation, including description, usage, examples, and runtime messages, is written only in Chinese. Under the policy, forcing a specific language without user opt-in or a clearly documented, justified locale restriction is a violation.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The module docstring, CLI description, argument help text, and error messages are all written in Chinese, which imposes a specific language on users. The file does not offer any language selection or indicate that the skill is intentionally limited to Chinese-speaking users or a China/HK-specific operational audience.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
91% confidence
Finding

The manifest description and user-facing title are written entirely in Chinese and present the skill usage as Chinese-only, with no indication that users may choose another language. Under the policy, locale or language constraints should be opt-in or explicitly justified; no such choice or justification appears here.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.