Back to skill

Security audit

Telegram Projects

Security checks for vulnerabilities and agentic risk

Overview

This skill appears intended to manage persistent Telegram project memory, but it can rewrite core agent instruction files using insufficiently validated local session data.

Review this before installing or running it. It should only be used by an operator who understands that it can modify persistent OpenClaw workspace memory and SOUL.md. Add strict chat ID validation, Markdown escaping, backups, and explicit approval before SOUL.md updates, and avoid loading Telegram-controlled names as instructions.

Vulnerability Patterns
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • 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
  • 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 (3)

T02 · Agent Memory Poisoning

Error
Location
scripts/sync-active-projects.py:67
Finding

Stored Prompt Injection Through Telegram Group Display Names

Content
View full analysis

Vulnerability Details

File Location: scripts/sync-active-projects.py:67-75, 199-209, 221-222
Vulnerability Type: Persistent prompt injection through untrusted metadata
Risk Level: High

Vulnerable Code

python
display_name = info.get("displayName", chat_id)
# Clean display name: "telegram:g-faya-golem_content" → "FAYA-GOLEM_content"
clean_name = display_name.replace("telegram:g-", "").replace("telegram:", "")

activity[chat_id] = {
    "last_active": datetime.fromtimestamp(updated_at / 1000).strftime("%Y-%m-%d"),
    "session_id": info.get("sessionId", ""),
    "display_name": clean_name,
}
python
else:
    name = get_project_name(chat_id)
    if name == chat_id:
        # Use display name from session if no project.md
        name = info.get("display_name", chat_id)
    size = "0"
    tier = "-"

# Check for actual project knowledge
if os.path.exists(os.path.join(PROJECTS_DIR, chat_id)):
    size, tier = get_knowledge_size(chat_id)

active_projects.append({
    "chat_id": chat_id,
    "name": name,
    "size": size,
    "tier": tier,
    "last_active": info["last_active"],
})
python
Path(SOUL_MD).write_text(new_content, encoding="utf-8")

The selected project name is subsequently interpolated into the active-project Markdown table:

python
for p in active_projects:
    new_section_lines.append(
        f"| {p['chat_id']} | {p['name']} | {p['size']} | {p['tier']} | {p['last_active']} |"
    )

Technical Analysis

The script reads displayName from Telegram session metadata and performs only two literal prefix removals. It does not reject or escape newline characters, Markdown headings, table delimiters, control characters, or instruction-like text.

For an active group without a project name, this value becomes p['name'] and is embedded directly in SOUL.md. The target file is not merely a display report: it is an ...[truncated 1866 chars]

Remediation
View remediation

Remediation Suggestions

  1. Do not write Telegram-controlled metadata into SOUL.md or any other instruction-bearing file.
  2. Store project state in a structured data file such as JSON, with a schema that distinguishes trusted instructions from untrusted display metadata.
  3. Render display names only in user-interface contexts where they are treated as quoted data.
  4. If Markdown generation remains necessary:
    • Remove carriage returns, newlines, and control characters.
    • Escape pipes, backticks, heading markers, HTML tags, and other Markdown metacharacters.
    • Enforce a conservative length limit.
    • Prefer a strict allowlist of ordinary Unicode letters, digits, spaces, underscores, and hyphens.
  5. Mark all externally sourced text explicitly as untrusted data when supplying it to an agent.
  6. Add tests using display names containing newlines, ## headings, table delimiters, code fences, and instruction-like content, and verify that none can create new Markdown blocks.
  7. Review and clean existing generated SOUL.md content for previously injected metadata.

T02 · Agent Memory Poisoning

Warning
Location
scripts/sync-active-projects.py:133
Finding

Synchronization Script Persistently Rewrites Core Agent Instructions

Content
View full analysis

Vulnerability Details

File Location: scripts/sync-active-projects.py:133-174, 207-209
Vulnerability Type: Unsafe persistent modification of agent behavioral state
Risk Level: Medium

Vulnerable Code

python
def update_soul_md(active_projects):
    """Update the Telegram Projects section in SOUL.md with active project list."""
    if not os.path.exists(SOUL_MD):
        print("SOUL.md not found!")
        return False

    content = Path(SOUL_MD).read_text(encoding="utf-8")

    # Build the new section
    new_section_lines = [
        "## Telegram Projects (always active)",
        "",
        "Every Telegram group can be a project with permanent knowledge. On **every message in a Telegram group:**",
        "",
        "1. Check `PROJECTS.md` for the current chat-id",
        '2. **If project exists:** Read `projects/{chat-id}/project.md` (always). Load `knowledge.md` per tier:',
        "   - **Tier 1 (< 5K tokens):** Load ALL knowledge",
        "   - **Tier 2 (5-20K):** Read glossary, extract ~5K relevant tokens for the current message",
        "   - **Tier 3 (> 20K):** Read glossary, extract 5-10K relevant tokens + vector search on demand",
        "3. **If no project exists:** Mention once per session that this channel can become a project",
        "4. Report knowledge size on first message after >1h pause: `📁 Project [name]: [size] loaded (Tier X)`",
        "5. For `/knowledge` or `/project` commands → read the `telegram-projects` SKILL.md for detailed rules",
        "",
        'Users add knowledge by saying "permanentes Knowledge: ..." or `/knowledge add [content]`.',
    ]

    if active_projects:
        new_section_lines.append("")
        new_section_lines.append("### Active Projects (last 4 weeks)")
        new_section_lines.append("")
        new_section_lines.append("| Chat ID | Name | Knowledge | Tier | Last Active |")
        new_section_li
...[truncated 2983 chars]
Remediation
View remediation

Remediation Suggestions

  1. Move active-project records and knowledge-tier data to a dedicated structured state file.
  2. Keep SOUL.md static and operator-controlled; synchronization jobs should not rewrite persona or instruction files.
  3. Implement project loading through trusted application code that reads structured records and treats all names and knowledge content as data.
  4. Require explicit administrative authorization before any operation modifies persistent agent instructions.
  5. If instruction updates are unavoidable, use a narrowly scoped template with integrity checks, backups, atomic replacement, and a reviewable change log.
  6. Validate that the replacement expression modifies exactly one expected section. Abort if zero or multiple matches occur.
  7. Separate trusted, fixed behavioral rules from generated project metadata so that untrusted values can never become instruction text.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/init-all-projects.py:31
Finding

Unvalidated Session Identifier Used as a Writable Filesystem Path

Content
View full analysis

Vulnerability Details

File Location: scripts/init-all-projects.py:31-38, 49-74
Vulnerability Type: Path traversal through an unvalidated project identifier
Risk Level: Medium

Vulnerable Code

python
parts = key.split(":")
if "topic" in parts:
    continue

try:
    group_idx = parts.index("group")
    chat_id = parts[group_idx + 1]
except (ValueError, IndexError):
    continue
python
display_name = info.get("displayName", chat_id)
clean_name = display_name.replace("telegram:g-", "").replace("telegram:", "")

project_dir = os.path.join(PROJECTS_DIR, chat_id)
os.makedirs(project_dir, exist_ok=True)

# Create project.md if not exists
project_md = os.path.join(project_dir, "project.md")
if not os.path.exists(project_md):
    last_active = datetime.fromtimestamp(updated_at / 1000).strftime("%Y-%m-%d")
    Path(project_md).write_text(
        f"# {clean_name}\n\n"
        f"- **Chat ID:** {chat_id}\n"
        f"- **Created:** {datetime.now().strftime('%Y-%m-%d')}\n"
        f"- **Last Active:** {last_active}\n"
        f"- **Description:** _(add a description for this project)_\n",
        encoding="utf-8"
    )

# Create knowledge.md if not exists
knowledge_md = os.path.join(project_dir, "knowledge.md")
if not os.path.exists(knowledge_md):
    Path(knowledge_md).write_text(
        f"# Project Knowledge: {clean_name}\n\n"
        f"_No knowledge entries yet. Add with: \"permanentes Knowledge: ...\" or `/knowledge add`_\n",
        encoding="utf-8"
    )

# Create glossary.md if not exists
glossary_md = os.path.join(project_dir, "glossary.md")
if not os.path.exists(glossary_md):
    Path(glossary_md).write_text(
        "# Project Glossary\n\n_No knowledge entries yet._\n",
        encoding="utf-8"
    )

Technical Analysis

The script extracts chat_id from a key in sessions.json but does not verify that it is a valid Telegram ...[truncated 2028 chars]

Remediation
View remediation

Remediation Suggestions

  1. Validate chat_id before any filesystem use. Telegram group identifiers should match a strict numeric pattern such as ^-?[0-9]+$.

  2. Reject empty values, separators, traversal components, control characters, and values exceeding a reasonable length.

  3. Resolve and verify the destination before creating it:

    python
    import re
    from pathlib import Path
    
    if not re.fullmatch(r"-?[0-9]+", chat_id):
        raise ValueError("Invalid Telegram chat ID")
    
    root = Path(PROJECTS_DIR).resolve()
    project_dir = (root / chat_id).resolve()
    
    if project_dir.parent != root:
        raise ValueError("Project path escapes the configured root")
    
  4. Check for symlinks in the destination path and refuse to follow them when creating or writing project files.

  5. Use atomic file creation with exclusive-create semantics where supported.

  6. Apply the same identifier validation in sync-active-projects.py and build-project-glossary.py, because those scripts also use project identifiers as path or generated-content components.

  7. Treat sessions.json as security-sensitive state and restrict it to the owning user with appropriate filesystem permissions.

Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Rogue AgentSelf-Modification, Session Persistence
  • 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
Findings (3)

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 · scripts/init-all-projects.py (reported line 3)May include surrounding context.

python
#!/usr/bin/env python3
"""
init-all-projects.py — Create project folders for all active Telegram groups.
"""

import os

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

The sample instruction explicitly says 'Wir sprechen Deutsch. Antworte immer kurz und präzise.', which promotes a fixed language choice. Because this file applies broadly to group onboarding and does not present language selection or opt-in, it conflicts with the policy against forcing a specific language by default.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

This code performs a file write that overwrites SOUL.md, which affects workspace state. While the module docstring states that it updates SOUL.md, there is no explicit user confirmation or stronger warning at the point of execution, and the script is configured to run daily via cron.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.