Back to skill

Security audit

Tistory Publish

Security checks for vulnerabilities and agentic risk

Overview

This skill is built for Tistory publishing, but it deserves Review because it can publish live posts, use account credentials, and has unsafe browser automation handling.

Install only if you are comfortable giving the skill control over an authenticated Tistory browser session. Use --private or a test blog first, keep any Kakao credential file outside shared repos with tight permissions, and do not run it on body HTML or --blog values from untrusted sources until the OG placeholder escaping and host validation issues are fixed.

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 (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/publish.sh:248
Finding
JavaScript Injection Through Untrusted OG Placeholder Values<![CDATA[ ## Vulnerability Details **File Location**: `scripts/publish.sh`, lines 248–251 **Vulnerability Type**: JavaScript injection through unsafe string interpolation **Risk Level**: High ### Vulnerable Code ```python og_urls = page.evaluate("typeof getOGPlaceholders === 'function' ? getOGPlaceholders() : []") log(f" - OG URLs: {len(og_urls)}") for url in og_urls: page.evaluate(f"prepareOGPlaceholder('{url}')") ``` ### Technical Analysis The `getOGPlaceholders()` helper extracts URL strings from `data-og-placeholder` attributes in the supplied post body. These values therefore originate from the HTML file selected through `--body-file`. Each extracted value is interpolated directly into a JavaScript source string: ```python page.evaluate(f"prepareOGPlaceholder('{url}')") ``` The value is not escaped as a JavaScript string literal. A placeholder containing a single quote followed by JavaScript syntax can terminate the intended argument and inject arbitrary code into the expression evaluated by Playwright. Although normal URLs are expected, `publish.sh` does not establish that the supplied body HTML is trusted or validate placeholder values before evaluating them. The injected expression executes in the context of the authenticated Tistory editor page. This issue differs from the Base64 pre-scan alert. The current Base64 logic in `scripts/tistory-publish.js` converts image bytes into a `Blob` and does not execute the decoded data. ### Attack Path 1. An attacker supplies or influences the HTML file passed through `--body-file`. 2. The HTML contains a crafted `data-og-placeholder` value with a single quote and injected JavaScript. 3. `tinymce.activeEditor.setContent()` inserts the HTML into the editor. 4. `getOGPlaceholders()` reads the attacker-controlled attribute and returns it to the Python process. 5. The value is concatenated into the source passed to `page.evaluate()`. 6. Playwright executes the injected JavaScript in the authenticated Tis ...[truncated 960 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Pass the placeholder as a Playwright evaluation argument instead of constructing JavaScript source: ```python for url in og_urls: page.evaluate("(url) => prepareOGPlaceholder(url)", url) ``` Apply additional input validation before using the value: ```python from urllib.parse import urlparse def validate_og_url(value): parsed = urlparse(value) if parsed.scheme not in ("http", "https") or not parsed.hostname: fail(f"invalid OG URL: {value}") if parsed.username or parsed.password: fail("credentials are not permitted in OG URLs") return value ``` Recommended hardening measures: 1. Treat all body HTML and placeholder attributes as untrusted input. 2. Never interpolate external values into JavaScript source strings. 3. Use Playwright’s structured argument serialization for every dynamic value. 4. Restrict placeholders to absolute `http` or `https` URLs. 5. Optionally enforce an allowlist of domains appropriate for the publishing workflow. 6. Add regression tests containing quotes, backslashes, line separators, and JavaScript-like placeholder values. 7. Reject malformed placeholders before any browser-side evaluation occurs. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/publish.sh:117
Finding
Unrestricted Blog Host Allows Out-of-Scope Browser Navigation and Script Injection<![CDATA[ ## Vulnerability Details **File Location**: `scripts/publish.sh`, lines 117–166 **Vulnerability Type**: Missing destination-host validation **Risk Level**: Medium ### Vulnerable Code ```python CDP_URL = f"http://127.0.0.1:{CDP_PORT}" NEWPOST_URL = f"https://{BLOG}/manage/newpost/?type=post" if BLOG else "https://www.tistory.com/manage/newpost/?type=post" ``` The resulting URL is later opened in the existing browser context, after which a local helper is injected: ```python page.goto(NEWPOST_URL, wait_until="domcontentloaded", timeout=30000) # 로그인 세션 확인 _login_domains = ["auth/login", "accounts.kakao.com", "logins.daum.net", "kauth.kakao.com"] if any(x in page.url for x in _login_domains): fail(f"카카오 로그인 세션 만료. scripts/login.sh 로 먼저 로그인하세요. (redirected: {page.url})") # TinyMCE 대기 log(" - tinymce 대기...") for i in range(20): ready = page.evaluate("typeof tinymce !== 'undefined' && tinymce.activeEditor && tinymce.activeEditor.initialized") if ready: break time.sleep(2) if not ready: fail("tinymce not ready after 40s") log("Step 1: 완료") # ── Helper JS 주입 (addScriptTag — CSP 우회) ── log("Injecting helper JS...") page.add_script_tag(path=HELPER_JS) time.sleep(1) if not page.evaluate("typeof insertContent === 'function'"): fail("helper JS injection failed") ``` ### Technical Analysis The `--blog` argument is inserted directly into the authority component of an HTTPS URL without parsing or validation. The declared functionality only requires navigation to Tistory and its blog subdomains, but the implementation accepts arbitrary hostnames. The script opens the resulting destination in an existing CDP browser context. If an attacker-controlled destination presents a compatible `tinymce` object, the readiness check can succeed and the local `tistory-publish.js` helper will be injected into that page. The login-redirect check is not an origin allowlist. It only rejects URLs containing a small set of login-related substring ...[truncated 1603 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Parse and validate `--blog` before constructing the destination URL. Permit only `tistory.com` and strict subdomains of `tistory.com`: ```python import re def validate_blog_host(host): host = host.strip().lower().rstrip(".") if not re.fullmatch( r"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*tistory\.com", host ): fail("blog must be tistory.com or a subdomain of tistory.com") if any(c in host for c in "/\\@:#?"): fail("blog must contain only a hostname") return host BLOG = validate_blog_host(BLOG) if BLOG else "" ``` Further hardening should include: 1. Parse destinations with a standard URL parser rather than relying on string concatenation. 2. Reject usernames, passwords, explicit ports, paths, fragments, query strings, IP literals, and lookalike suffixes. 3. After every navigation and redirect, verify that `page.url` has the `https` scheme and an allowed Tistory hostname. 4. Perform the origin check again immediately before injecting the helper or submitting content. 5. Consider creating a dedicated browser context for publishing rather than reusing a general-purpose context. 6. Abort if the page origin changes during publication. 7. Document that `--blog` accepts a hostname only, not an arbitrary URL. ]]>
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 (19)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The description frames the skill as blog publishing automation, but the content also includes credential-file parsing and Kakao login/session recovery. That is security-relevant behavior because it handles authentication secrets and account access; if users are not clearly warned, they may provide credentials under a narrower trust assumption than the skill actually requires.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The description frames the skill as blog publishing automation, but the content also includes credential-file parsing and Kakao login/session recovery. That is security-relevant behavior because it handles authentication secrets and account access; if users are not clearly warned, they may provide credentials under a narrower trust assumption than the skill actually requires.

Credential Access

High
Category
Privilege Escalation
Content
# publish.sh 실행 전 로그인 세션이 만료됐을 때 사용
#
# 사용:
#   bash scripts/login.sh --cred-file /path/to/credentials.json [--cdp-port 18800]
#
# 자격증명 파일 형식 (JSON 또는 key: value):
#   {"email": "...", "password": "..."}
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
# publish.sh 실행 전 로그인 세션이 만료됐을 때 사용
#
# 사용:
#   bash scripts/login.sh --cred-file /path/to/credentials.json [--cdp-port 18800]
#
# 자격증명 파일 형식 (JSON 또는 key: value):
#   {"email": "...", "password": "..."}
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
# publish.sh 실행 전 로그인 세션이 만료됐을 때 사용
#
# 사용:
#   bash scripts/login.sh --cred-file /path/to/credentials.json [--cdp-port 18800]
#
# 자격증명 파일 형식 (JSON 또는 key: value):
#   {"email": "...", "password": "..."}
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The markdown clearly states that the skill automatically publishes posts to Tistory, which affects user-controlled remote content. Under the markdown-file criteria, the description should warn about impacts to user data or system integrity, but no cautionary note about live publication, overwriting mistakes, or remote upload effects is present.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The first usage example shows a direct `publish.sh` invocation against a real blog domain, but the README does not disclose that running the command can immediately create or publish a post on the user's account. For markdown guidance that can affect user data or privacy, a visible warning is expected.

Lp3

Medium
Category
MCP Least Privilege
Confidence
87% confidence
Finding
The skill advertises network and file-reading capabilities but does not declare any explicit tool scope or permission boundaries. In a skill that automates browser actions and optionally interacts with local credential files, missing scope declarations increases the chance of unintended file access or network actions occurring without clear user awareness or policy enforcement.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The documentation describes credential-based session recovery using email/password files but does not provide a strong user-facing warning about secret handling, storage risks, or privacy implications. Because this skill targets a real Kakao/Tistory account and may restore authenticated sessions, insufficient disclosure increases the risk of unsafe credential practices and unintended account compromise.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill automates actions that can publish content, upload media, and modify a live Tistory blog, but the description lacks a prominent warning that these are write operations against a production account. In this context, silent or poorly disclosed state-changing automation is risky because a user may trigger irreversible blog changes, reputational damage, or accidental disclosure.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This code loads email and password credentials from a user-supplied file, then later uses them to authenticate against Kakao/Tistory. Although the script has usage comments and status prints, it does not clearly disclose that it processes highly sensitive credentials and transmits them during automated login.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The script documentation and command examples are entirely in Korean, and later runtime messages and UI assumptions also rely on Korean text. This imposes a specific language/locale on users without any opt-in or documented justification, which matches the language-policy violation criteria.

Natural-Language Policy Violations

Medium
Confidence
91% confidence
Finding
The automation searches for Korean UI text such as "첨부", "사진", "완료", "비공개 저장", and "공개 발행" to perform actions. This means the skill only works in a Korean locale and does not provide any user choice or documented opt-in for that constraint.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The script contains functions that directly activate the final publication flow, including clicking the public publish button, without any built-in confirmation, preview checkpoint, or user-consent gate. In this skill’s context, the code is explicitly designed to automate posting to a live blog while also working around trusted-event restrictions, so accidental or unauthorized publication of content is materially more likely.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The runbook instructs the user to run a publishing command that can create or modify content on a live Tistory blog, and only later briefly notes that removing `--private` enables real publication. Because this skill is explicitly designed to automate remote blog publishing and editor manipulation, failing to foreground the live-impact behavior can lead to unintended publication or remote data changes by users who follow the example verbatim or adapt it without understanding the consequences.

Natural-Language Policy Violations

Low
Confidence
92% confidence
Finding
This markdown file contains user-facing natural language in Korean, but there is no indication that the skill is intentionally Korean-only or that users can opt into another language. Under the policy rule, forcing a specific language without opt-in is a natural-language policy concern.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
The user-facing comments, usage guidance, and runtime messages are presented exclusively in Korean. Under the stated policy, forcing a single language without opt-in or alternative locale support is a natural-language policy violation.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
The file's natural-language instructions, comments, and UI matching strings are written to operate only in Korean and assume Korean-language interface text such as '완료', '공개 발행', '첨부', and '사진'. There is no indication that the user can opt into this locale constraint or select another language.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The natural-language instructions, headings, and option descriptions are all presented in Korean, which can amount to forcing a specific language on users without opt-in. The policy allows locale constraints only when the skill offers a language choice or clearly documents a justified regional limitation, which is not present here.

Static analysis

No suspicious patterns detected.