Back to skill

Security audit

bytedance-visual-recognition

Security checks for vulnerabilities and agentic risk

Overview

The skill does what it claims, but it under-protects and may retain sensitive media context and API credentials longer or more openly than users would expect.

Review before installing if you may process private images, videos, screenshots, documents, or high-value API keys. Use least-privileged provider keys, avoid shared or synced folders, manually delete .last_response and vision_history.json when done, and do not rely on the documented seven-day cleanup for GLM follow-up context.

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

Warning
Location
doubao_vision_recognize.py:72
Finding

Plaintext Credential Storage Without Restrictive File Permissions

Content
View full analysis

Vulnerability Details

File Location: doubao_vision_recognize.py, lines 72–96 and 118–126
Vulnerability Type: Plaintext sensitive credential storage
Risk Level: Medium

Vulnerable Code

python
# First run: auto-generate config.json with plaintext storage warning
if not _CONFIG_PATH.exists():
    meta = {"_comment": "⚠️ API keys stored in plaintext! Do not use on shared/backed-up/synced machines. Fill in values below, leave blank to disable."}
    config = dict(meta, **_CONFIG_DEFAULTS)
    with open(_CONFIG_PATH, "w", encoding="utf-8", newline="\n") as _f:
        json.dump(config, _f, ensure_ascii=False, indent=2)
        _f.write("\n")

# Load config
_cfg = {}
if _CONFIG_PATH.exists():
    try:
        with open(_CONFIG_PATH, "r", encoding="utf-8") as _f:
            _cfg = json.load(_f)
    except Exception:
        pass

API_URL = "https://ark.cn-beijing.volces.com/api/v3/responses"
ARK_API_KEY = _cfg.get("ark_api_key", "")
IAM_AK = _cfg.get("volcengine_access_key", "")
IAM_SK = _cfg.get("volcengine_secret_key", "")
ZHIPU_API_URL = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
ZHIPU_API_KEY = _cfg.get("zhipu_api_key", "")

The same file is subsequently rewritten without enforcing restrictive permissions:

python
try:
    with open(_CONFIG_PATH, "r", encoding="utf-8") as _f:
        data = json.load(_f)
    data["privacy_notice_shown"] = True
    with open(_CONFIG_PATH, "w", encoding="utf-8", newline="\n") as _f:
        json.dump(data, _f, ensure_ascii=False, indent=2)
        _f.write("\n")
except Exception:
    pass

Technical Analysis

The Skill stores the Volcengine Ark API key, Volcengine IAM access key and secret key, and Zhipu API key as plaintext values in config.json. Although the user-facing documentation warns about plaintext storage, the implementation does not enforce owner-only permissions when creating or rewriti ...[truncated 2005 chars]

Remediation
View remediation

Remediation Suggestions

  1. Prefer an operating-system credential manager, secret manager, or runtime environment variables instead of persistent plaintext JSON values.
  2. If file-based storage must remain supported:
    • Create the credential file atomically with owner-only mode 0600 on POSIX systems.
    • Verify and reject or warn about group/world-readable permissions before loading credentials.
    • Preserve restrictive permissions whenever the file is rewritten.
    • Apply equivalent access-control protections on Windows.
  3. Separate non-sensitive preferences from credentials so updating privacy_notice_shown does not rewrite a secret-bearing file.
  4. Avoid storing IAM access and secret keys unless console synchronization is explicitly enabled by the user.
  5. Require a least-privileged IAM principal restricted to the usage-query action needed by the Skill.
  6. Ensure config.json is excluded from source control, package publication, logs, support bundles, backups, and synchronization by default.
  7. Add credential rotation guidance for users who suspect that the file has been exposed.

T09 · Insecure Skill Coding Practices

Warning
Location
doubao_vision_recognize.py:721
Finding

Unbounded Plaintext Retention of Base64-Encoded GLM Media Context

Content
View full analysis

Vulnerability Details

File Location: doubao_vision_recognize.py, lines 631–633 and 721–746
Vulnerability Type: Insecure plaintext retention of sensitive media
Risk Level: Medium

Vulnerable Code

The initial GLM request places the complete media data URI into the saved message collection:

python
if provider == "zhipu":
    blocks = self._build_zhipu_blocks(media, data_uri, full_prompt, sp)
    messages = [{"role": "user", "content": blocks}]
    result, error = self._zhipu_call(model, messages, retries=model["retries"])

The complete message collection, including the Base64 media data URI, is then persisted to .last_response:

python
def _save_glm_context(self, resp_id: str, model_id: str, input_type: str,
                       file_path: str, messages: List[Dict], assistant_text: str,
                       mode: str):
    """Save GLM multi-turn chat context (message history + last reply) for follow-up"""
    try:
        data = {"resp_id": resp_id, "model_id": model_id,
                "input_type": input_type, "file": file_path,
                "provider": "zhipu", "mode": mode,
                "messages": messages,
                "last_text": assistant_text,
                "ts": time.time()}
        tmp = LAST_RESP_FILE.with_suffix(".json.tmp")
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        tmp.replace(LAST_RESP_FILE)
    except Exception:
        pass

def _load_last_resp(self) -> Optional[Dict]:
    try:
        if LAST_RESP_FILE.exists():
            with open(LAST_RESP_FILE, "r", encoding="utf-8") as f:
                return json.load(f)
    except Exception:
        pass
    return None

Technical Analysis

_encode_media() Base64-encodes the complete selected image or video and creates a data URI. For the GLM backend, this data URI is included in `mess ...[truncated 2587 chars]

Remediation
View remediation

Remediation Suggestions

  1. Enforce the configured retention period for .last_response:
    • Check ts whenever context is loaded.
    • Delete and reject context older than temp_retention_days.
    • Run the same expiration check during initialization and cleanup.
  2. Add an explicit command to clear follow-up context immediately.
  3. Avoid persisting complete Base64 media where the provider offers a server-side conversation or file reference.
  4. If GLM follow-up requires resending media, obtain explicit user consent for local persistence and store only the minimum context necessary.
  5. Create .last_response with owner-only permissions and verify its permissions before reading it.
  6. Consider encrypting persisted media context using an operating-system-protected key.
  7. Ensure .last_response is excluded from repositories, package artifacts, logs, backups, and synchronization by default.
  8. Update SKILL.md so its retention statement precisely matches implemented behavior, and add automated tests confirming that expired GLM context is removed.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (5)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding

Without declared permissions the skill's intent is opaque and cannot be validated.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The manifest sets locales: ["en"], which imposes an English-only locale constraint. The file does not offer users a language choice or explain why the restriction is necessary for a region-specific or compliance-bound use case.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The follow-up flow reuses previously uploaded image/video context and prior conversation state from .last_response without giving the user a fresh warning at execution time. In this skill’s context, media may contain sensitive visual data and the tool explicitly persists history and cached media locally, so a user can unintentionally send follow-up prompts that continue processing prior sensitive content after the initial recognition step.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

The built-in system prompts and default prompt assembly are written to instruct the model in English, including fixed output-format expectations such as Markdown and English default instructions. This imposes a language/locale choice without offering the user an explicit option to select their preferred language.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
87% confidence
Finding

When no user prompt is provided, the skill appends fixed English instructions such as 'Describe the content in detail' and 'Generate frontend code based on the content'. This hard-codes an English interaction mode rather than offering a language choice or deferring to the user's locale.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.