Back to skill

Security audit

Kakaotalk

Security checks for vulnerabilities and agentic risk

Overview

This KakaoTalk agent is coherent for a chatbot, but its relay/local modes expose private memory, callbacks, credentials, and a persistent local service with insufficient safeguards.

Install only after reviewing Relay mode carefully. Use a dedicated low-privilege account or machine, set webhook authentication, restrict callback destinations to Kakao-controlled HTTPS hosts, avoid sending MEMORY.md to external LLMs unless explicitly intended, rotate any secrets written into a plist, and avoid enabling the launchd service until uninstall and log-retention controls are clear.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • System PersistenceInstalls backdoors, hooks, services, or scheduled tasks that survive the run
  • 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
Findings (5)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/watcher.py:89
Finding

Unauthenticated Relay Can Exfiltrate Private Agent Memory to an Attacker-Controlled Callback

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/server.py:279
Finding

Optional Authentication and Unrestricted Callback URL Enable Blind SSRF

Content
View full analysis
bool: """KAKAO_CALLBACK_SECRET 기반 HMAC-SHA1 검증. 시크릿 없으면 스킵.""" if not KAKAO_SECRET: return True expected = hmac.new( KAKAO_SECRET.encode("utf-8"), body, hashlib.sha1, ).hexdigest() return hmac.compare_digest(expected, signature or "") ``` ```python # scripts/server.py:279-294 def _send_callback(callback_url: str, text: str) -> None: """LLM 완료 후 카카오 콜백 URL로 실제 응답 전송.""" try: payload = json.dumps(_kakao_response(text, include_quick_replies=False), ensure_ascii=False).encode("utf-8") req = urllib.request.Request( callback_url, data=payload, headers={"Content-Type": "application/json; charset=utf-8"}, method="POST", ) with urllib.request.urlopen(req, timeout=10) as resp: _log(f"📤 콜백 전송 완료: status={resp.status}, url={callback_url[:60]}") except Exception as e: _log(f"❌ 콜백 전송 실패: {e}") ``` ```python # scripts/server.py:353-365 # utterance / user_id / callbackUrl 추출 user_request = body.get("userRequest", {}) utterance = user_request.get("utterance", "").strip() user_id = user_request.get("user", {}).get("id", "unknown") callback_url = user_request.get("callbackUrl", "") # AI 챗봇 모드에서만 존재 _log(f"📩 user={user_id[:12]}... | utterance={utterance[:60]} | callback={'✅' if callback_url else '❌'}") if not utterance: self._send_json(_kakao_response("메시지를 입력해주세요 😊")) return ``` ```python # scripts/server.py:443-450 if _callback_url and not FORCE_SYNC: if response: _send_callback(_callback_url, response) else: _send_callba ...[truncated 2044 chars]
Remediation
View remediation

T06 · System Persistence

Error
Location
scripts/install-service.sh:47
Finding

Installer Registers a Cross-Session Auto-Starting launchd Service

Content
View full analysis
"$PLIST_PATH" << PLIST Label ${LABEL} ProgramArguments ${PYTHON3} -u ${SCRIPT_PATH} EnvironmentVariables KAKAOTALK_PORT 8401 OLLAMA_HOST http://localhost:11434 GEMINI_API_KEY ${GEMINI_API_KEY_VAL} KAKAO_CALLBACK_SECRET ${KAKAO_SECRET_VAL} PATH /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin WorkingDirectory ${SKILL_DIR} KeepAlive SuccessfulExit StandardOutPath ${LOG_FILE} StandardErrorPath ${ERR_FILE} RunAtLoad ThrottleInterval 10 PLIST chmod 644 "$PLIST_PATH" echo "✅ plist 생성: $PLIST_PATH" launchctl load -w "$PLIST_PATH" ``` ### Technical Analysis The installer writes a LaunchAgent into `~/Library/LaunchAgents`, configures it with `RunAtLoad`, and enables automatic restart following unsuccessful termination through `KeepAlive`. It then loads the service with `launchctl load -w`. The persistence is related to operating an always-on local KakaoTalk webhook and is visibly implemented rather than concealed. However, it exceeds the privileges required by Basic/Vercel mode and causes the ...[truncated 1130 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/install-service.sh:13
Finding

Gemini API Key and Callback Secret Are Duplicated into a World-Readable Plist

Content
View full analysis
EnvironmentVariables KAKAOTALK_PORT 8401 OLLAMA_HOST http://localhost:11434 GEMINI_API_KEY ${GEMINI_API_KEY_VAL} KAKAO_CALLBACK_SECRET ${KAKAO_SECRET_VAL} ``` ```bash # scripts/install-service.sh:104 chmod 644 "$PLIST_PATH" ``` ### Technical Analysis The installer reads two secrets from `~/.openclaw/.env`, embeds them directly into a plaintext LaunchAgent plist, and sets the plist mode to `0644`. This creates a second long-lived secret copy that is readable by other local users whenever parent-directory traversal permissions permit access. The values are interpolated into XML without escaping. Characters with special XML meaning can corrupt the plist or alter its structure. The extraction logic also removes every double quote rather than parsing a defined environment-file format safely. ### Attack Path 1. A user stores a Gemini API key and Kakao callback secret in `~/.openclaw/.env`. 2. The user runs the installer. 3. The script copies both values into `~/Library/LaunchAgents/com.yeomyeonggeori.kakaotalk.plist`. 4. The script changes the plist permissions to `0644`. 5. Another local account, backup process, synchronization tool, or diagnostic ...[truncated 590 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/server.py:102
Finding

Webhook Messages and User Identifiers Are Persistently Logged Without Retention or Permission Controls

Content
View full analysis
None: """파일 + stdout 동시 출력.""" LOG_DIR.mkdir(parents=True, exist_ok=True) ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") line = f"[{ts}] {msg}" print(line, flush=True) try: with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(line + "\n") except Exception: pass ``` ```python # scripts/server.py:367 _log(f"📩 user={user_id[:12]}... | utterance={utterance[:60]} | callback={'✅' if callback_url else '❌'}") ``` ```bash # scripts/install-service.sh:9-11 LOG_DIR="${KAKAOTALK_LOG_DIR:-$HOME/.openclaw/logs}" LOG_FILE="$LOG_DIR/kakaotalk.log" ERR_FILE="$LOG_DIR/kakaotalk.err.log" ``` ```xml StandardOutPath ${LOG_FILE} StandardErrorPath ${ERR_FILE} ``` ### Technical Analysis For every ordinary webhook request, the server logs a truncated user identifier and the first 60 characters of the user's message. The same output is written to standard output and an append-only local file. Under launchd, standard output and standard error are also redirected to persistent files. The code does not explicitly create logs with restrictive permissions, rotate them, limit retention, redact sensitive message content, or prevent log injection through control characters embedded in an utterance. Chat messages frequently contain personal, financial, business, or authentication information, making message-body logging sensitive by default. ### Attack Path 1. A user sends confidential information through the KakaoTalk chatbot. 2. The server records the first 60 characters and a stable portion of the user's identifier. ...[truncated 856 chars]
Remediation
View remediation
Vulnerability Patterns
  • 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
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
Findings (71)

Tainted flow: 'req' from os.environ.get (line 70, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · api/index.py (reported line 47)May include surrounding context.

python
method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=5) as resp:
            return resp.status in (200, 201)
    except Exception as e:
        print(f"DB Error: {e}")

Tainted flow: 'req' from os.environ.get (line 70, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · api/index.py (reported line 73)May include surrounding context.

python
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}, method="POST")

    try:
        with urllib.request.urlopen(req, timeout=9) as resp:
            data = json.load(resp)
            return data["candidates"][0]["content"]["parts"][0]["text"].strip()
    except Exception as e:

Tainted flow: 'req' from os.environ.get (line 275, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/server.py (reported line 157)May include surrounding context.

python
method="POST",
    )

    with urllib.request.urlopen(req, timeout=OLLAMA_TIMEOUT) as resp:
        data = json.load(resp)
        text = data["message"]["content"].strip()
        return _strip_thinking_tags(text)

Tainted flow: 'req' from os.environ.get (line 275, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/server.py (reported line 201)May include surrounding context.

python
method="POST",
    )

    with urllib.request.urlopen(req, timeout=30) as resp:
        data = json.load(resp)
        return data["candidates"][0]["content"]["parts"][0]["text"].strip()

Tainted flow: 'req' from os.environ.get (line 275, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
94% confidence
Finding

The server blindly POSTs to callbackUrl taken from the inbound request body, with no allowlist or origin validation. If signature verification is disabled or bypassed, an attacker can supply an arbitrary URL and coerce the server into making outbound requests, creating an SSRF primitive and potential internal network reachability.

Content

Scanner excerpt · scripts/server.py (reported line 281)May include surrounding context.

python
headers={"Content-Type": "application/json; charset=utf-8"},
            method="POST",
        )
        with urllib.request.urlopen(req, timeout=10) as resp:
            _log(f"📤 콜백 전송 완료: status={resp.status}, url={callback_url[:60]}")
    except Exception as e:
        _log(f"❌ 콜백 전송 실패: {e}")

Tainted flow: 'req' from os.environ.get (line 124, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/watcher.py (reported line 43)May include surrounding context.

python
"Authorization": f"Bearer {SUPABASE_KEY}"
    })
    try:
        with urllib.request.urlopen(req) as resp:
            return json.load(resp)
    except Exception as e:
        print(f"Poll Error: {e}")

Tainted flow: 'req' from os.environ.get (line 124, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/watcher.py (reported line 59)May include surrounding context.

python
"Content-Type": "application/json"
    })
    try:
        with urllib.request.urlopen(req):
            pass
    except Exception as e:
        print(f"Update Error: {e}")

Tainted flow: 'req' from os.environ.get (line 124, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

The code POSTs agent responses to a callback URL taken from queued message data without validating the destination. Because responses may contain model output influenced by local memory, this creates an SSRF/exfiltration path where an attacker who controls callback_url can receive sensitive data or force requests to arbitrary internal or external endpoints.

Content

Scanner excerpt · scripts/watcher.py (reported line 79)May include surrounding context.

python
"Content-Type": "application/json"
    })
    try:
        with urllib.request.urlopen(req) as resp:
            print(f"✅ 발송 성공: {text[:20]}...")
            return True
    except Exception as e:

Tainted flow: 'req' from os.environ.get (line 124, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
91% confidence
Finding

This call transmits user messages together with injected local MEMORY.md contents to an external Gemini API. That creates a real data exfiltration risk because private local context is sent off-host and may later be reflected back in responses, exceeding what a user would expect from a messaging-channel watcher.

Content

Scanner excerpt · scripts/watcher.py (reported line 127)May include surrounding context.

python
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}, method="POST")
    
    try:
        with urllib.request.urlopen(req) as resp:
            data = json.load(resp)
            return data["candidates"][0]["content"]["parts"][0]["text"].strip()
    except Exception as e:

External Script Fetching

High
Category
Supply Chain
Confidence
90% confidence
Finding

Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Content

Scanner excerpt · references/kakao-api.md (reported line 211)May include surrounding context.


테스트 curl 예시

bash
# 기본 대화 테스트

External Script Fetching

High
Category
Supply Chain
Confidence
90% confidence
Finding

Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Content

Scanner excerpt · references/kakao-api.md (reported line 227)May include surrounding context.

md
}' | python3 -m json.tool

# 세션 초기화
curl -s -X POST http://localhost:8401/kakao \
  -H "Content-Type: application/json" \
  -d '{
    "userRequest": {"utterance": "처음으로", "user": {"id": "test_user_001"}},

External Script Fetching

High
Category
Supply Chain
Confidence
90% confidence
Finding

Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Content

Scanner excerpt · references/kakao-api.md (reported line 236)May include surrounding context.

md
}' | python3 -m json.tool

# "다시 물어보기" (LLM 타임아웃 후 재조회)
curl -s -X POST http://localhost:8401/kakao \
  -H "Content-Type: application/json" \
  -d '{
    "userRequest": {"utterance": "다시 물어보기", "user": {"id": "test_user_001"}},

Credential Access

High
Category
Privilege Escalation
Confidence
95% confidence
Finding

The script accesses a .env file expected to contain credentials, then later extracts and uses those secrets for service configuration. In this skill's context, the credentials include an API key and callback secret, so local disclosure could enable unauthorized API usage or forged webhook requests depending on downstream validation.

Content

Scanner excerpt · scripts/install-service.sh (reported line 13)May include surrounding context.

sh
LOG_FILE="$LOG_DIR/kakaotalk.log"
ERR_FILE="$LOG_DIR/kakaotalk.err.log"
PLIST_PATH="$HOME/Library/LaunchAgents/${LABEL}.plist"
ENV_FILE="$HOME/.openclaw/.env"

# ─── 환경변수 로드 ─────────────────────────────────────────────────────────────
if [[ -f "$ENV_FILE" ]]; then

External Script Fetching

High
Category
Supply Chain
Confidence
90% confidence
Finding

Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Content

Scanner excerpt · scripts/ngrok-setup.sh (reported line 65)May include surrounding context.

sh
PUBLIC_URL=""
for i in $(seq 1 20); do
  sleep 0.5
  PUBLIC_URL=$(curl -sf http://127.0.0.1:4040/api/tunnels 2>/dev/null \
    | python3 -c "
import sys, json
try:

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

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

md
# ─── 설정 ─────────────────────────────────────────────────────────────────────

# .env 로드 (간이)
def load_env():
    env_path = os.path.expanduser("~/.openclaw/.env")
    if os.path.exists(env_path):

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/watcher.py (reported line 14)May include surrounding context.

python
# ─── 설정 ─────────────────────────────────────────────────────────────────────

# .env 로드 (간이)
def load_env():
    env_path = os.path.expanduser("~/.openclaw/.env")
    if os.path.exists(env_path):

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/watcher.py (reported line 16)May include surrounding context.

python
# .env 로드 (간이)
def load_env():
    env_path = os.path.expanduser("~/.openclaw/.env")
    if os.path.exists(env_path):
        with open(env_path) as f:
            for line in f:

Missing User Warnings

High
Category
Not specified by scanner
Confidence
97% confidence
Finding

User messages and local memory are forwarded to an external LLM API with no visible disclosure or consent flow. That is dangerous because private operator data may leave the host and user content may be processed by a third party unexpectedly, with additional risk of the model echoing memory back in replies.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
Not specified by scanner
Confidence
96% confidence
Finding

The code sends user messages and local memory contents to Google's Gemini API, creating third-party disclosure of potentially sensitive local context. Given the skill description, optional local access does not imply silent export of that data to an external model provider, so the capability is more dangerous in this context.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill advertises and instructs use of capabilities including environment variables, shell commands, network deployment, and local file interaction, but it does not declare an explicit permission or allowed-tools scope. That creates a trust and review gap: operators may install or run it without understanding the effective power it requires, especially because Relay mode bridges remote chat input to a local machine.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The description explicitly says the skill can connect to a local computer for file and memory access, but it does not provide a clear upfront warning about privacy, data exposure, or system-impact risks. Because the feature is exposed through a messaging channel, users may underestimate that chat requests could reach sensitive local resources.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The documentation markets a feature where KakaoTalk messages can request reading files from the user's computer, yet it gives no explicit safety constraints, authorization model, or warning about sensitive data access. In context, this is more dangerous because a remote messaging interface is being tied to a local watcher, which can turn ordinary chat prompts into access to private workstation data.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
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.

Content

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

  1. Supabase 프로젝트 생성.
  2. SQL Editor에서 아래 쿼리 실행:
    sql
    create table kakaotalk_queue (
      id bigint generated by default as identity primary key,
      user_id text not null, utterance text not null, callback_url text,
      status text default 'pending', response text, created_at timestamptz default now()
    

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The default system prompt is written in Korean and instructs the assistant how to answer, which imposes a language/locale behavior by default. There is no indication in this file that users can opt into another language or that the Korean-only behavior is a documented regional requirement.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

In relay mode, the code transmits user_id, utterance, and callback_url to Supabase without any visible user-facing notice or consent mechanism in this file. Because this is a chat integration and the skill description mentions optional local computer connectivity, the privacy risk is elevated: users may not reasonably expect their messages and callback metadata to be queued in an external backend for later processing.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.