Back to skill

Security audit

Upbit Trading Skill

Security checks for vulnerabilities and agentic risk

Overview

The skill is not clearly malicious, but it asks for exchange credentials and runs an unreviewed external script through unsafe shell execution while overstating what the trading bot actually does.

Install only after reviewing and fixing the external GLM execution path and shell invocation. Use restricted Upbit API keys with no withdrawal permission, prefer a test or low-balance account, and do not rely on the advertised automated-trading or Telegram behavior unless the missing pieces are supplied and reviewed.

Vulnerability Patterns
  • Tool Hijacking and SpoofingModifies or replaces tools so legitimate-looking calls execute attacker logic
  • 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
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
realtime-bot.js:43
Finding
Shell Command Injection Through Position Market Data<![CDATA[ ## Vulnerability Details **File Location**: `realtime-bot.js`, lines 43-50 and 116-164 **Vulnerability Type**: OS command injection **Risk Level**: High ### Vulnerable Code ```js function askGLM(prompt) { try { const result = execSync( `cd ${__dirname}/../zai && ./ask.sh "${prompt.replace(/"/g, '\\"')}" glm-4.7`, { encoding: 'utf8', timeout: 30000 } ); return result.trim(); } catch (err) { log(`GLM 호출 실패: ${err.message}`); return null; } } ``` The prompt passed to this function incorporates data read from `positions.json`: ```js const data = loadJSON(POSITIONS_FILE, { positions: [] }); const positions = data.positions || []; const openPositions = positions.filter(p => p.status === 'open'); for (const pos of openPositions) { const currentPrice = await getPrice(pos.market); const entryPrice = pos.entryPrice || pos.avgPrice; const pnlPercent = (currentPrice - entryPrice) / entryPrice; const prompt = `당신은 암호화폐 트레이딩 봇입니다. 포지션: ${pos.market} 진입가: ${entryPrice}원 현재가: ${currentPrice}원 손익: ${(pnlPercent * 100).toFixed(2)}% 목표: +5%, 손절: -5% 다음 중 하나만 답하세요: 1. HOLD - 유지 2. SELL_NOW - 즉시 매도 (목표/손절 전이라도) 3. ADJUST:새목표,새손절 - 목표/손절 조정 응답 형식: HOLD, SELL_NOW, 또는 ADJUST:5,-3`; const response = askGLM(prompt); ``` ### Technical Analysis `execSync()` receives a dynamically constructed command string and consequently invokes a shell. The code attempts to protect the prompt by escaping double quotes, but this does not make content safe inside a shell double-quoted argument. Shell constructs including command substitution remain active within double quotes. For example, `$(command)` and backtick substitutions can still be evaluated. Because `pos.market` is loaded from the locally writable `positions.json` file and interpolated into the prompt without strict validation, a malicious market value can introduce shell syntax. This is an injection flaw rather than merely malformed argument handling: attacker-controlled te ...[truncated 1597 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Eliminate shell command construction. Use `execFileSync()` or `spawnSync()` with an argument array: ```js const { execFileSync } = require('child_process'); function askGLM(prompt) { try { return execFileSync( path.join(__dirname, '..', 'zai', 'ask.sh'), [prompt, 'glm-4.7'], { encoding: 'utf8', timeout: 30000, shell: false } ).trim(); } catch (err) { log(`GLM invocation failed: ${err.message}`); return null; } } ``` 2. Avoid using `cd` in a command string. Set the child process `cwd` option instead. 3. Validate every market identifier before using it: ```js const MARKET_PATTERN = /^KRW-[A-Z0-9]+$/; if (!MARKET_PATTERN.test(pos.market)) { log('Rejected invalid market identifier'); continue; } ``` 4. Validate the complete structure and data types of `positions.json` against a schema. 5. Run the bot under a dedicated, minimally privileged operating-system account. 6. Restrict read and write permissions on `.env`, `positions.json`, and executable integration files. 7. Add regression tests using values containing `$()`, backticks, quotes, semicolons, newlines, and shell metacharacters to confirm that they are passed only as literal arguments. ]]>

T07 · Tool Hijacking and Spoofing

Error
Location
realtime-bot.js:43
Finding
Execution of an Unverified Script Outside the Audited Skill Directory<![CDATA[ ## Vulnerability Details **File Location**: `realtime-bot.js`, lines 43-50 **Vulnerability Type**: Untrusted external tool execution **Risk Level**: High ### Vulnerable Code ```js function askGLM(prompt) { try { const result = execSync( `cd ${__dirname}/../zai && ./ask.sh "${prompt.replace(/"/g, '\\"')}" glm-4.7`, { encoding: 'utf8', timeout: 30000 } ); return result.trim(); } catch (err) { log(`GLM 호출 실패: ${err.message}`); return null; } } ``` ### Technical Analysis The bot executes `../zai/ask.sh`, which resides outside the audited project directory. The referenced script is not included in the provided artifact, is not described as a required executable dependency in `SKILL.md`, and cannot be reviewed as part of this Skill. No integrity check, ownership check, permission check, pinned version, or trusted absolute installation path is enforced before execution. Therefore, the behavior of market analysis depends on whichever file happens to occupy that sibling path at runtime. The external script also receives prompts containing market identifiers, entry prices, current prices, and profit or loss information. Because the script implementation is unavailable, its processing and network destinations cannot be verified. ### Attack Path 1. An attacker compromises the installation process, shared workspace, extracted archive layout, or another component with permission to create or replace `../zai/ask.sh`. 2. The attacker places a malicious executable script at that expected path. 3. A user starts `realtime-bot.js`, believing it will perform the documented market analysis. 4. After the configured 30-second analysis interval, `askGLM()` changes into the sibling directory and executes the substituted script. 5. The malicious script runs with the bot process's permissions and receives the generated financial-analysis prompt. 6. The script can execute arbitrary local actions or transmit received data elsewhere. ## ...[truncated 650 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Include the required integration within the reviewed package or replace it with a documented API client implemented in the project. 2. Pin the integration to a reviewed version and verify its cryptographic digest before execution. 3. Use a trusted absolute path rather than a mutable sibling-directory path. 4. Verify that the executable is a regular file, is owned by the expected user, and is not writable by untrusted users or groups. 5. Invoke the executable using `execFileSync()` or `spawnSync()` with `shell: false`. 6. Document all information sent to the AI integration, its network endpoint, retention policy, and credential requirements. 7. Apply least-privilege controls to the child process, including a restricted environment, minimal filesystem access, and network restrictions where feasible. 8. Fail closed when the expected executable or integrity metadata is missing rather than executing an arbitrary file found at the path. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
realtime-bot.js:55
Finding
Unbounded Repeated Event Generation Can Exhaust Local Resources<![CDATA[ ## Vulnerability Details **File Location**: `realtime-bot.js`, lines 55-65, 78-111, and 218-221 **Vulnerability Type**: Unbounded file growth and local denial of service **Risk Level**: Medium ### Vulnerable Code ```js function addEvent(event) { const events = loadJSON(EVENTS_FILE, []); events.push({ ...event, timestamp: new Date().toISOString(), processed: false }); saveJSON(EVENTS_FILE, events); log(`📢 이벤트 추가: ${event.type} - ${event.message}`); } ``` Threshold checks generate another event on every polling cycle while the condition remains true: ```js if (pnlPercent >= CONFIG.targetProfit) { addEvent({ type: 'TARGET_HIT', market: pos.market, entryPrice, currentPrice, pnlPercent, message: `🎯 ${pos.market} 목표 도달! +${(pnlPercent * 100).toFixed(2)}%` }); } else if (pnlPercent <= CONFIG.stopLoss) { addEvent({ type: 'STOPLOSS_HIT', market: pos.market, entryPrice, currentPrice, pnlPercent, message: `🚨 ${pos.market} 손절 도달! ${(pnlPercent * 100).toFixed(2)}%` }); } ``` The check runs continuously: ```js mainLoop(); setInterval(mainLoop, CONFIG.priceCheckInterval); ``` ### Technical Analysis When an open position remains at or beyond the configured profit or loss threshold, each ten-second polling cycle appends another event for the same condition. There is no state-transition check, deduplication key, cooldown, maximum event count, file-size limit, archival policy, or log rotation. In addition, `addEvent()` reads and parses the entire event file, appends one object, serializes the entire array, and rewrites the whole file. Processing cost therefore increases as the file grows. Repeated events can progressively consume disk space, memory, and CPU. `setInterval()` also does not wait for a previous asynchronous `mainLoop()` call to finish before scheduling another invocation. Slow network or AI operations can consequently produce overlapping iterations and further resourc ...[truncated 1384 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Emit threshold events only when a position transitions from below to above a target, or from above to below a stop-loss boundary. 2. Store per-position alert state, such as `targetAlerted` and `stopLossAlerted`, and reset it only after the condition clears or the position changes. 3. Deduplicate events using a stable key combining event type, market, position identifier, and threshold state. 4. Introduce a cooldown if repeated notifications are operationally required. 5. Enforce retention limits and rotate or archive old events. 6. Prefer append-only structured logging or a bounded database rather than reading and rewriting the entire history. 7. Check available file size and handle failed or partial writes safely, ideally through atomic temporary-file replacement. 8. Replace `setInterval()` with a self-scheduling loop that begins the next iteration only after the previous one completes: ```js async function runContinuously() { while (true) { const started = Date.now(); try { await mainLoop(); } catch (error) { log(`Main loop failed: ${error.message}`); } const delay = Math.max( 0, CONFIG.priceCheckInterval - (Date.now() - started) ); await new Promise(resolve => setTimeout(resolve, delay)); } } ``` 9. Add monitoring and alerts for event-file size, write failures, iteration duration, and overlapping work. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (18)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
Advertising automated trading and technical analysis while those functions are not implemented, combined with undeclared local reads/writes of trade-related JSON state, creates a transparency and trust problem. Users may believe the bot is executing vetted strategies when it is instead performing different behavior, including persistence of potentially sensitive trading data.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
Advertising automated trading and technical analysis while those functions are not implemented, combined with undeclared local reads/writes of trade-related JSON state, creates a transparency and trust problem. Users may believe the bot is executing vetted strategies when it is instead performing different behavior, including persistence of potentially sensitive trading data.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
Advertising automated trading and technical analysis while those functions are not implemented, combined with undeclared local reads/writes of trade-related JSON state, creates a transparency and trust problem. Users may believe the bot is executing vetted strategies when it is instead performing different behavior, including persistence of potentially sensitive trading data.

Credential Access

High
Category
Privilege Escalation
Content
2. 환경변수 설정:

```bash
cp .env.example .env
# UPBIT_ACCESS_KEY, UPBIT_SECRET_KEY 입력
```
Confidence
85% confidence
Finding
The documentation explicitly instructs users to create a .env file containing exchange API access and secret keys. In a financial automation skill, requesting high-value credentials is inherently sensitive; if the skill is misleading or mishandles secrets, users could lose account confidentiality or authorize unwanted trading actions.

Ae1

High
Category
analysis-evasion
Content
- `realtime-bot.js` - 메인 봇
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Credential Access

High
Category
Privilege Escalation
Content
#!/usr/bin/env node
// 업비트 잔고 조회

require('dotenv').config({ path: __dirname + '/.env' });
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill advertises functionality that would require environment-variable access and network connectivity, but it does not explicitly declare tool scope or permissions. In an automation/trading context, this weakens transparency and reviewability, making it harder for users and platforms to understand what sensitive capabilities the skill expects to use.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill promotes automated real-time trading but omits warnings about financial loss, unintended orders, and account impact. In a live trading context, the absence of risk disclosure makes unsafe use more likely because users may treat the tool as low-risk automation rather than software that can directly affect assets.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The setup instructions tell users to place Upbit API credentials into a .env file without explaining secure handling, least-privilege API scopes, or the risk of granting trading authority. Because exchange API keys can expose balances and enable orders, weak credential guidance materially increases the chance of account compromise or misuse.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The script accesses exchange API credentials via environment variables to authenticate against a live Upbit account. While this is part of the script's functionality, there is no doc comment, warning, or other user-facing disclosure that the skill consumes sensitive credentials.

External Transmission

Medium
Category
Data Exfiltration
Content
const token = jwt.sign(payload, SECRET_KEY);
  
  try {
    const response = await axios.get('https://api.upbit.com/v1/accounts', {
      headers: { Authorization: `Bearer ${token}` }
    });
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
const token = jwt.sign(payload, SECRET_KEY);
  
  try {
    const response = await axios.get('https://api.upbit.com/v1/accounts', {
      headers: { Authorization: `Bearer ${token}` }
    });
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
This code sends an authenticated request containing a bearer token derived from the user's API keys to api.upbit.com. Although balance retrieval is the apparent purpose, the file itself does not include any warning or disclosure that account data will be transmitted to an external service.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The manifest description says '자동매매' (automated trading), implying the skill performs buy/sell actions. In this file, the code fetches prices, queries GLM, and appends signals like TARGET_HIT, GLM_SELL_SIGNAL, and GLM_ADJUST to events.json, but never calls any Upbit authenticated trading endpoint or otherwise executes an order.

Natural-Language Policy Violations

Medium
Confidence
91% confidence
Finding
The log function hard-codes the locale to 'ko-KR' and timezone to 'Asia/Seoul'. This is a natural-language/locale policy issue because the skill enforces a specific locale presentation without offering user opt-in or documenting that the tool is intentionally region-specific.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
Natural-language content in the manifest and README is presented exclusively in Korean, including the description, features, and setup steps. This can violate language/locale policy expectations when no user opt-in or explicit region-specific justification is provided.

Natural-Language Policy Violations

Low
Confidence
92% confidence
Finding
This JavaScript file contains user-facing and instructional natural language exclusively in Korean, including comments and CLI test output. Under the language/locale policy rule, forcing a specific language without user opt-in or justification can be a policy violation.

Intent-Code Divergence

Low
Confidence
82% confidence
Finding
The file header states 'Upbit 실시간 모니터링 봇' and documents price checks, GLM analysis, and event logging only. That documentation conflicts with the broader skill-level claim of '자동매매', creating an intent/code documentation divergence about whether the bot merely monitors or actually trades.

Static analysis

Detected: suspicious.dangerous_exec, suspicious.env_credential_access

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
realtime-bot.js:53

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
balance.js:9