Back to skill

Security audit

TG Canvas Mini App

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly coherent and disclosed, but it gives approved Telegram users powerful server shell access and executes pushed HTML scripts, so it needs careful review before installation.

Install only if you intentionally want a Telegram-accessible server terminal and fully trust every ID in ALLOWED_USER_IDS with shell access to the host. Run it as a dedicated unprivileged user or isolated container, keep PUSH_TOKEN/JWT_SECRET/BOT_TOKEN strong and private, avoid running as root, consider disabling or removing terminal/script execution, and do not use the detached OpenClaw tunnel script unless the upstream service has its own strong access control.

Vulnerability Patterns
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (7)

T09 · Insecure Skill Coding Practices

Error
Location
miniapp/app.js:424
Finding

Stored Active-Content Injection Executes Arbitrary JavaScript

Content
View full analysis
{ const s = document.createElement('script'); if (oldScript.src) s.src = oldScript.src; s.type = oldScript.type || 'text/javascript'; s.text = oldScript.textContent || ''; oldScript.replaceWith(s); }); } ``` ### Technical Analysis The `/push` endpoint accepts arbitrary HTML and stores it in the global canvas state. The frontend inserts that content through `innerHTML` and then deliberately recreates every embedded `
Remediation
View remediation
` elements. - Sanitize HTML with a maintained strict allowlist before storage and again before rendering. - Reject scripts, event-handler attributes, `javascript:` URLs, unsafe SVG content, iframes, and active embedded objects. - Prefer rendering untrusted HTML in a sandboxed iframe on a separate origin without `allow-scripts` or `allow-same-origin`. - Add a restrictive Content Security Policy that disallows inline scripts and limits script sources. - Separate content-publishing authorization from general server secrets and rotate `PUSH_TOKEN` after any suspected exposure. - Consider supporting only Markdown, text, and a constrained declarative UI format. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
server.js:574
Finding

Normal Canvas Authentication Grants an Always-Enabled Server Shell

Content
View full analysis
{ try { return os.userInfo(); } catch (_) { return null; } })(); const userHome = userInfo?.homedir || process.env.HOME || "/"; const userName = userInfo?.username || process.env.USER || ""; if (process.getuid && process.getuid() === 0) { console.warn("[tg-canvas] WARNING: server is running as root; terminal will be a root shell. " + "Set User= in the systemd service to fix this."); } let term; try { term = pty.spawn(shell, [], { name: "xterm-256color", cols, rows, cwd: userHome, env: { ...process.env, HOME: userHome, USER: userName, LOGNAME: userName, TERM: "xterm-256color", }, }); ``` `server.js:620-625`: ```js ws.on("message", (raw) => { try { const msg = JSON.parse(raw.toString()); if (msg.type === "data" && typeof msg.data === "string") { term.write(msg.data); ``` `server.js:771-790`: ```js if (url.pathname !== "/ws" && url.pathname !== "/ws/terminal") { socket.destroy(); return; } const ip = req.socket.remoteAddress || "unknown"; if (!rateLimit(`ws:${ip}`, RATE_LIMIT_AUTH_PER_MIN, 60_000)) { socket.write("HTTP/1.1 429 Too Many Requests\r\n\r\n"); socket.destroy(); return; } const token = url.searchParams.get("token") || ""; const payload = verifyJwt(token); if (!payload) { socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); socket.destroy(); return; } if (url.pathname === "/ws/terminal") { termWss.handleUpgrade(req, socket, head, (ws) => { term ...[truncated 2039 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/tunnel-openclaw-control.sh:16
Finding

Detached Public Tunnel Bypasses the JWT-Gated OpenClaw Proxy

Content
View full analysis
"$LOG" nohup cloudflared tunnel --url http://127.0.0.1:18789 >>"$LOG" 2>&1 & PID=$! echo "$PID" > "$PIDFILE" for i in {1..60}; do URL=$(grep -Eo 'https://[-a-zA-Z0-9]+\.trycloudflare\.com' "$LOG" | tail -n1 || true) if [[ -n "$URL" ]]; then echo "OpenClaw Control tunnel started" echo "PID: $PID" echo "URL: $URL" exit 0 fi sleep 1 if ! kill -0 "$PID" >/dev/null 2>&1; then echo "cloudflared exited early. Check $LOG" >&2 tail -n 40 "$LOG" >&2 exit 1 fi done echo "Timed out waiting for tunnel URL. Check $LOG" >&2 exit 1 ``` ### Technical Analysis The script creates a Cloudflare quick tunnel directly to the local OpenClaw gateway on port 18789. It does not route through the application's JWT-protected `/oc/*` proxy and adds no Cloudflare Access policy or other tunnel-layer authentication. `nohup` and background execution allow the tunnel to continue after the script exits or the initiating session disconnects. The PID is written to a file, but the project provides no corresponding stop or cleanup operation. The script does not verify that the upstream OpenClaw service has strong native authentication before publishing it. ### Attack Path 1. A user or Agent invokes `scripts/tunnel-openclaw-control.sh`. 2. The script starts a detached Cloudflare tunnel to `127.0.0.1:18789`. 3. Cloudflare creates a public URL, bypassing the tg-canvas `/oc/*` JWT gate. 4. The script exits successfully while the tunnel remains active in the background. 5. The URL is leaked through terminal output, logs, shell records, messaging, or operational monitoring. 6. An attacker connects directly to the OpenClaw Control UI. 7. If upstream a ...[truncated 483 chars]
Remediation
View remediation

T03 · Remote Payload Retrieval and Execution

Warning
Location
miniapp/app.js:255
Finding

Privileged Browser Code Is Retrieved and Executed from External CDNs

Content
View full analysis
``` `miniapp/app.js:255-267`: ```js function loadScript(src) { return new Promise((resolve, reject) => { if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; } const s = document.createElement('script'); s.src = src; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); } // Lazy-load xterm.js and FitAddon from CDN try { await loadScript('https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js'); await loadScript('https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js'); ``` ### Technical Analysis The authenticated Mini App downloads and executes JavaScript from external infrastructure at runtime. No Subresource Integrity hash is provided, and the page has no restrictive Content Security Policy. Although the xterm version appears fixed in the URL, the delivered artifact is still controlled by the remote CDN and package publishing chain after the Skill itself has been reviewed. The terminal libraries are loaded immediately before establishing a highly privileged shell session, making compromise of this delivery channel particularly consequential. ### Attack Path 1. The CDN, package account, package artifact, or delivery path is compromised. 2. An approved user opens the Mini App and selects Terminal. 3. The browser downloads the modified xterm or FitAddon script. 4. The script executes ...[truncated 606 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
miniapp/app.js:311
Finding

Session JWTs Are Exposed in HTTP and WebSocket URLs

Content
View full analysis
{ const url = `/oc/?token=${encodeURIComponent(jwt)}`; window.location.assign(url); }, 80); ``` ```js async function fetchState() { try { const res = await fetch(`/state?token=${encodeURIComponent(jwt)}`); ``` ```js const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${proto}//${location.host}/ws?token=${encodeURIComponent(jwt)}`; setStatus('connecting'); ws = new WebSocket(wsUrl); ``` ### Technical Analysis The same JWT authorizes canvas state, WebSocket access, the OpenClaw proxy, and the server terminal. It is repeatedly included in URL query strings. URLs may be retained by browser history, reverse proxies, Cloudflare diagnostics, access logs, monitoring systems, crash reports, and operational tooling. Using a query parameter is unnecessary for `/state`, where an authorization header can be used. WebSocket browser APIs complicate custom headers, but this does not justify reusing a high-value bearer token directly in the URL. ### Attack Path 1. An approved user authenticates and receives a JWT. 2. The frontend uses the JWT in state, canvas WebSocket, terminal WebSocket, or Control UI URLs. 3. Infrastructure or browser tooling records the complete URL. 4. An attacker with access to those records extracts the token before its expiration. 5. The attacker reuses it against `/ws/terminal` or another protected endpoint. 6. The server accepts the bearer token without device binding or a one-time-use restriction. ### Impact Assessme ...[truncated 287 chars]
Remediation
View remediation

other

Note
Location
server.js:479
Finding

Health Endpoint Is Public Despite Being Documented as Loopback-Only

Content
View full analysis
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
package.json:12
Finding

Startup Installs Mutable npm Dependencies Without a Lockfile

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
Findings (38)

YARA rule 'agent_skill_credential_exfiltration_webhook': AI agent skill credential harvesting followed by webhook or external exfiltration [agent_skills]

Critical
Category
YARA Match
Confidence
85% confidence
Finding

YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).

Content

Scanner excerpt · scripts/setup-bot.js (reported line 29)May include surrounding context.

js
const idx = line.indexOf('=');
    if (idx === -1) continue;
    const key = line.slice(0, idx).trim();
    let val = line.slice(idx + 1).trim();
    if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
      val = val.slice(1, -1);
    }
    out[key] = val;
  }
  return out;
}

async function callTelegram(token, method, payload) {
  const url = `https://api.telegram.org/bot${token}/${method}`;
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok || !json.ok) {
    const errMsg = json?.description || res.statusText || 'Unknown error';
    throw new Error(`${method} failed: ${errMsg}`);
  }
  return json;
}

async function main() {
  const envFromFile = parseDotEnv(path.resolve(process.cwd(), '.env'));
  const BOT_TOKEN = process.env.BOT_TOKEN || envFromFile.BOT_TOKEN;
  const MINIAPP_URL

Chaining Abuse

High
Category
Tool Misuse
Confidence
75% confidence
Finding

Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.

Content

Scanner excerpt · README.md (reported line 95)May include surrounding context.

bash
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx

2) Certbot (webroot):

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 · README.md (reported line 223)May include surrounding context.

bash
sudo mkdir -p /etc/tg-canvas
sudo cp .env /etc/tg-canvas/.env
  1. Update the unit file to point at the env file location:

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 · README.md (reported line 229)May include surrounding context.

bash
sudo mkdir -p /etc/tg-canvas
sudo cp .env /etc/tg-canvas/.env
  1. Update the unit file to point at the env file location:

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
ent variable. The relevant code is the `ENABLE_OPENCLAW_PROXY` config block in `server.js` — verify it yourself.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
ent variable. The relevant code is the `ENABLE_OPENCLAW_PROXY` config block in `server.js` — verify it yourself.

Intent-Code Divergence

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

The client renders server-provided HTML with innerHTML and then explicitly re-executes both inline and external

Content

No source excerpt is available for this finding.

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/setup-bot.js (reported line 44)May include surrounding context.

js
}

async function main() {
  const envFromFile = parseDotEnv(path.resolve(process.cwd(), '.env'));
  const BOT_TOKEN = process.env.BOT_TOKEN || envFromFile.BOT_TOKEN;
  const MINIAPP_URL = process.env.MINIAPP_URL || envFromFile.MINIAPP_URL;

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 94)May include surrounding context.

text

```bash
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 95)May include surrounding context.

text

```bash
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 101)May include surrounding context.

text

```bash
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 108)May include surrounding context.

text

```bash
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 215)May include surrounding context.

text

```bash
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 216)May include surrounding context.

text

```bash
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 222)May include surrounding context.

text

```bash
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 223)May include surrounding context.

text

```bash
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 235)May include surrounding context.

text

```bash
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 236)May include surrounding context.

text

```bash
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx

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 · README.md (reported line 94)May include surrounding context.

text

```bash
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo nginx -t && sudo systemctl reload nginx

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · README.md (reported line 175)May include surrounding context.

Use the CLI or the HTTP /push API (loopback-only):

bash
curl -X POST http://127.0.0.1:3721/push \
  -H 'Content-Type: application/json' \
  -d '{"html":"<h1>Hello Canvas</h1>"}'

Session Persistence

Medium
Category
Rogue Agent
Confidence
80% 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 · README.md (reported line 235)May include surrounding context.

  1. Enable and start:
bash
sudo systemctl enable --now tg-canvas
sudo systemctl status tg-canvas

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 262)May include surrounding context.

md
- **`PUSH_TOKEN` is required** — the server exits at startup without it. The loopback IP check alone is insufficient when `cloudflared` is active.
- **Terminal** grants real shell access. Only list users in `ALLOWED_USER_IDS` who should have shell access to your server.
- **Proxy** is off by default (`ENABLE_OPENCLAW_PROXY` must be set to `"true"` explicitly). No local credential files are read automatically.
- Keep `.env` permissions tight (`chmod 600 .env`) and rotate secrets if compromised.

## Canvas Learnings (from live testing)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill declares no explicit tool scope or permission boundaries even though the described implementation uses environment variables and network-facing server functionality. In a server skill that exposes authentication, proxying, and a browser-accessible terminal, missing capability declarations reduce reviewability and can cause operators or orchestration systems to grant broader access than intended.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

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

text
- HTTP API:
  ```bash
  curl -X POST http://127.0.0.1:3721/push \
    -H 'Content-Type: application/json' \
    -d '{"html":"<h1>Hello</h1>"}'

Unrestricted Tool Access

Medium
Category
Excessive Agency
Confidence
98% confidence
Finding

The skill intentionally exposes a server-side PTY/bash over the network to authenticated Telegram users, which is effectively remote shell access to the host as the server process user. Even if access is restricted to ALLOWED_USER_IDS and JWT-authenticated sessions, compromise of an allowed Telegram account, bot auth flow, or JWT secret would immediately translate into arbitrary command execution on the server.

Content

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

md
The Mini App includes an interactive terminal backed by a server-side PTY.

> ⚠️ **This grants shell access to the machine running the server**, as the process user. Anyone in `ALLOWED_USER_IDS` can open a bash session and run arbitrary commands. Only add users you trust with shell access to `ALLOWED_USER_IDS`.

**How it works:**
- Authenticated users see a **Terminal** button in the Mini App topbar.

Static analysis

Detected: suspicious.env_credential_access, suspicious.exposed_secret_literal

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
bin/tg-canvas.js:7

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
server.js:17

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
server.js:161