Back to skill

Security audit

Ipo Alert

Security checks for vulnerabilities and agentic risk

Overview

This is a disclosed Korean IPO alert skill that fetches fixed 38.co.kr pages, prints schedule alerts, and keeps a small local state file to avoid duplicate notifications.

Install only if you want Korean IPO alerts from 38.co.kr and are comfortable with the skill fetching that site and storing alert history in ~/.config/ipo-alert/state.json. Treat alert text as external website data, review any cron or HEARTBEAT automation before enabling it, and clear the state file if you want to reset notification history.

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
check_ipo.py:48
Finding
Untrusted Remote Content Is Emitted as Agent-Facing Markdown Without Sanitization<![CDATA[ ## Vulnerability Details **File Location**: `check_ipo.py:48-55`, `check_ipo.py:84-91`, and `check_ipo.py:166-177` **Vulnerability Type**: Untrusted content injection into Markdown-formatted notifications **Risk Level**: Medium ### Complete Code Snippet ```python # Subscription data obtained from the remote website item_id = match.group(1).strip() name = match.group(2).strip() date_range = match.group(3).strip() confirmed_price = match.group(4).strip().replace("&nbsp;", "").strip() expected_price = match.group(5).strip().replace("&nbsp;", "").strip() competition = match.group(6).strip().replace("&nbsp;", "").strip() underwriter = match.group(7).strip().replace("&nbsp;", "").strip() # Listing data obtained from the remote website item_id = match.group(1).strip() name = match.group(2).strip() name = re.sub(r'<font[^>]*>[^<]*</font>', '', name).strip() listing_date_str = match.group(3).strip() current_price = match.group(4).strip().replace("&nbsp;", "").strip() ipo_price = match.group(5).strip().replace("&nbsp;", "").strip() def format_subscription(item: dict) -> str: """Format subscription item for notification.""" price = item.get("confirmed_price") or item.get("expected_price") or "-" date_str = format_date_with_weekday(item['date_range'], item['start_date']) url = item.get('url', '') name_with_link = f"[{item['name']}]({url})" if url else item['name'] return f"📋 {name_with_link}\n 청약: {date_str}\n 공모가: {price}\n 주간사: {item['underwriter']}" def format_listing(item: dict) -> str: """Format listing item for notification.""" price = item.get("ipo_price") or "-" try: listing_date = datetime.fromisoformat(item['listing_date']).date() wd = get_weekday_kr(listing_date) date_str = f"{listing_date.month:02d}/{listing_date.day:02d}({wd})" except: date_str = item['listing_date'] url = item.get('url', '') name_with_link = f"[{item['name']}]({url})" if url else item['name'] ...[truncated 2845 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Escape Markdown metacharacters before formatting remote values.** At minimum, escape characters such as `\`, `[`, `]`, `(`, `)`, `*`, `_`, `` ` ``, `~`, `>`, and `#`. 2. **Apply strict field validation.** - Limit company and underwriter names to expected Unicode letters, digits, spaces, and a small allowlist of punctuation. - Validate prices against an expected numeric and separator format. - Reject unexpected line breaks and control characters. - Enforce reasonable maximum lengths for every extracted field. 3. **Separate untrusted data from agent instructions.** Mark fetched values explicitly as external data and ensure downstream prompts state that notification content must not be interpreted as commands or policy instructions. 4. **Prefer plain-text output where rich Markdown is unnecessary.** If Markdown links are retained, construct all link destinations exclusively from locally validated identifiers and escape the visible label. 5. **Use a dedicated sanitization helper**, for example: ```python def sanitize_text(value: str, max_length: int = 120) -> str: value = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", value) value = value.replace("\r", " ").replace("\n", " ") value = re.sub(r"\s+", " ", value).strip() value = value[:max_length] for char in ("\\", "[", "]", "(", ")", "*", "_", "`", "~", ">", "#"): value = value.replace(char, "\\" + char) return value ``` 6. **Apply sanitization immediately after parsing** so every later formatter receives normalized data rather than raw external content. 7. **Add security tests** containing Markdown links, multiline instruction text, control characters, oversized fields, and malformed Unicode to verify that generated alerts remain inert. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (8)

Ae1

High
Category
analysis-evasion
Content
| `SKILL.md` | 이 문서 |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill instructs the agent or user to run a Python script and create a persistent state directory, which implies shell execution and file writes, but it does not declare any explicit tool scope or permissions. This creates an authorization and review gap: an agent may invoke shell/file capabilities broader than users expect, increasing the chance of unintended command execution or filesystem modification.

Vague Triggers

Medium
Confidence
93% confidence
Finding
The manifest trigger list includes very short and generic terms such as "IPO", "청약", and "상장" without any scope constraints or exclusion conditions. In a manifest file, these broad triggers can cause unintended activation during ordinary discussion of listings or subscriptions rather than explicit requests to run this skill.

Session Persistence

Medium
Category
Rogue Agent
Content
## 설치 후 설정

1. 상태 파일 디렉토리 생성: `mkdir -p ~/.config/ipo-alert`
2. 크론잡 또는 HEARTBEAT.md에 체크 추가

## 스크립트
Confidence
76% confidence
Finding
The skill persists state in `~/.config/ipo-alert/state.json`, which means data survives across sessions and can influence future behavior. Persistent local state can become a security issue if it is not clearly disclosed, constrained, or protected, because it may enable tracking, stale state manipulation, or unintended retention of user-related activity over time.

Context-Inappropriate Capability

Medium
Confidence
90% confidence
Finding
The manifest describes a skill for collecting IPO/listing schedules from 38.co.kr and providing alerts and summaries. Network access is expected for that purpose, but spawning an external program through `subprocess.run` introduces process-execution capability that is not justified by the stated alerting function.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def fetch_page(url: str) -> str:
    """Fetch page with EUC-KR encoding."""
    result = subprocess.run(
        ["curl", "-s", url],
        capture_output=True
    )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Session Persistence

Medium
Category
Rogue Agent
Content
return {"notified_subscriptions": [], "notified_listings": [], "last_check": None}

def save_state(state: dict):
    """Save state to file."""
    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2, ensure_ascii=False)
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The module docstring and command descriptions are entirely in Korean, and the script later emits Korean-only user-facing messages. For an all-file-types policy check, this is a natural-language locale constraint with no opt-in or explanation that the skill is intentionally limited to Korean-speaking users.

Static analysis

No suspicious patterns detected.