Back to skill

Security audit

NEIS School CLI

Security checks for vulnerabilities and agentic risk

Overview

This skill is a coherent Korean school lookup tool with a low-risk implementation note around optional API-key handling.

Install only if you are comfortable sending school lookup details to the official NEIS service. If you use NEIS_API_KEY on a shared or closely monitored machine, be aware that the fallback curl path could expose that key through local process-argument logging or inspection.

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

Note
Location
scripts/neis_cli.py:197
Finding

Optional API Key Exposed Through Curl Process Arguments

Content
View full analysis

Vulnerability Details

File Location: scripts/neis_cli.py, lines 197-212 and 226-240
Vulnerability Type: Sensitive information exposure through process command-line arguments
Risk Level: Low

Vulnerable Code

python
def optional_api_key() -> str | None:
    api_key = os.environ.get("NEIS_API_KEY", "").strip()
    return api_key or None


class NeisClient:
    def fetch(self, endpoint: str, params: dict[str, Any]) -> dict[str, Any]:
        query = {"Type": "json", "pIndex": 1, "pSize": 100}
        query.update({key: value for key, value in params.items() if value is not None and value != ""})
        if self.api_key:
            query["KEY"] = self.api_key
        url = f"{API_BASE_URL}/{endpoint}?{urllib.parse.urlencode(query)}"
python
@staticmethod
def _default_open(url: str) -> str:
    try:
        with urllib.request.urlopen(url, timeout=10) as response:
            return response.read().decode("utf-8")
    except urllib.error.URLError as exc:
        # Some local Python runtimes in macOS shells fail DNS resolution even though curl works.
        if not isinstance(exc.reason, OSError):
            raise
        try:
            result = subprocess.run(
                ["curl", "-fsSL", url],
                check=True,
                capture_output=True,
                text=True,
            )

Technical Analysis

The optional NEIS_API_KEY is inserted into the query string of the HTTPS request URL. If urllib.request.urlopen() raises a qualifying URLError, the implementation invokes curl and supplies the complete authenticated URL as a command-line argument.

Command-line arguments can be observable through local process-inspection facilities, monitoring agents, audit logs, crash diagnostics, or process telemetry. Consequently, a local party with access to such facilities may capture the API key while the fallback curl process is running.

The subprocess invocation uses an argument list rather than a shell co ...[truncated 1523 chars]

Remediation
View remediation

Remediation Suggestions

  1. Remove the curl fallback and return a controlled connection error when the standard Python HTTPS client fails.
  2. If curl must remain, prevent the authenticated URL from appearing in its argument vector. Supply sensitive configuration through standard input or another mechanism that does not expose the key through process arguments, after verifying the selected mechanism is not logged.
  3. Prefer authentication in an HTTP header if the NEIS API supports it. If query-string authentication is mandatory, ensure complete URLs are never logged, included in exceptions, or passed through observable command-line arguments.
  4. Keep the API base URL fixed and continue using list-form subprocess invocation without shell=True.
  5. Add a regression test that configures NEIS_API_KEY, triggers the fallback, intercepts subprocess.run, and verifies that no subprocess argument contains the key.
  6. Document that school lookup parameters and the optional credential are transmitted to the official NEIS service over HTTPS.
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • 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
Findings (7)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding

The skill invokes a local Python script and, by design, relies on shell execution, file reads, environment access, and outbound network calls, but it declares no explicit tool scope or permissions boundary. That means an agent platform may grant broader capabilities than users expect, making misuse or accidental overreach harder to constrain or audit, especially if the underlying script changes over time.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

This code's natural-language strings for the CLI description, help text, and error messages are consistently Korean-only, which effectively forces a specific language on all users. The file does not provide any opt-in, locale selection, or documented justification for restricting interaction to Korean.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The manifest describes a skill for querying school information from the NEIS OpenAPI, which justifies outbound HTTP requests but not spawning local subprocesses. The fallback in _default_open invokes curl via subprocess.run, adding an execution capability unrelated to the stated user-facing purpose.

Content

No source excerpt is available for this finding.

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/neis_cli.py (reported line 235)May include surrounding context.

python
if not isinstance(exc.reason, OSError):
                raise
            try:
                result = subprocess.run(
                    ["curl", "-fsSL", url],
                    check=True,
                    capture_output=True,

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 · tests/test_cli.py (reported line 73)May include surrounding context.

python
self.assertEqual([row["period"] for row in rows], ["1", "2", "3"])

    def test_main_prints_help(self):
        result = subprocess.run(
            ["python3", str(SCRIPT_PATH), "--help"],
            capture_output=True,
            text=True,

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
91% confidence
Finding

The JSON contains a natural-language response string only in Korean ("해당하는 데이터가 없습니다.") with no indication that language selection is optional or region-specific. Because SQP-3 applies to all file types, a hard-coded locale-specific message can be a policy concern if the skill is expected to support broader users without explicit opt-in.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
91% confidence
Finding

The string literal "정상 처리되었습니다." is a natural-language message presented only in Korean. For a file type covered by SQP-3, this can indicate a language/locale policy issue because the fixture encodes a fixed language without any visible opt-in or justification in the file.

Content

No source excerpt is available for this finding.