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## 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
## 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)

Lp3

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.

Natural-Language Policy Violations

Medium
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.

Context-Inappropriate Capability

Medium
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.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if not isinstance(exc.reason, OSError):
                raise
            try:
                result = subprocess.run(
                    ["curl", "-fsSL", url],
                    check=True,
                    capture_output=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
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,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Low
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.

Natural-Language Policy Violations

Low
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.

Static analysis

No suspicious patterns detected.