Back to skill

Security audit

Canvas Os 1.0.1

Security checks for vulnerabilities and agentic risk

Overview

This Canvas app skill is mostly purpose-aligned, but it should be reviewed because its scripts can expose local folders, kill unrelated local processes, and inject JavaScript into Canvas without strong scoping.

Install only if you are comfortable with a skill that starts local web servers, injects JavaScript into Canvas, and lets Canvas apps send messages back to the agent. Use it only with trusted app content and trusted data, avoid arbitrary app names or ports, and review the scripts before allowing process cleanup or serving local directories.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (6)

T09 · Insecure Skill Coding Practices

Error
Location
canvas-inject.py:18
Finding
JavaScript Injection Through Incomplete Template-Literal Escaping<![CDATA[ ## Vulnerability Details **File Location**: `canvas-inject.py:18-25` **Additional Locations**: `CANVAS-LOADING.md:40-50`, `SKILL.md:131-139` **Vulnerability Type**: JavaScript code injection caused by unsafe code generation **Risk Level**: High ### Vulnerable Code ```python # Escape backticks in HTML (they break the JS template literal) html_escaped = html_content.replace('`', '\\`') # JavaScript to inject HTML js_code = f"""document.open(); document.write(`{html_escaped}`); document.close();""" ``` The same unsafe construction is recommended in the documentation: ```python html_escaped = html_content.replace('`', '\\`') js_code = f"""document.open(); document.write(`{html_escaped}`); document.close();""" ``` ### Technical Analysis The helper embeds `html_content` inside a JavaScript template literal but escapes only backtick characters. It does not neutralize JavaScript template-literal interpolation sequences such as `${...}`, nor does it robustly encode backslashes and other JavaScript-significant input. Consequently, attacker-controlled HTML containing a value such as `${maliciousExpression()}` is interpreted as JavaScript during evaluation rather than being treated exclusively as document content. The generated string is subsequently sent to the Canvas `eval` operation, providing a direct execution sink. Although the intended feature permits rendering active HTML, this flaw causes input to execute while the privileged injection program is being evaluated, before normal document parsing and outside the expected data boundary. ### Attack Path 1. An attacker influences HTML passed to `inject_html_to_canvas()`, for example through externally sourced content used to construct a dashboard. 2. The HTML includes a JavaScript template-literal interpolation expression such as `${...}`. 3. The helper escapes backticks but leaves the interpolation expression intact. 4. The returned `step2_inject` command is submitted to Canvas `eval`. 5. Canvas ...[truncated 632 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Do not place HTML inside a JavaScript template literal. Serialize the content as a JavaScript string using a standards-compliant encoder: ```python import json js_code = ( "document.open();" f"document.write({json.dumps(html_content)});" "document.close();" ) ``` Additional hardening should include: 1. Treat externally sourced HTML as untrusted. 2. Sanitize HTML with an allowlist-based sanitizer if scripts and active attributes are not required. 3. Prefer a structured Canvas API that accepts HTML as data instead of generating JavaScript. 4. Remove the unsafe template-literal example from `CANVAS-LOADING.md` and `SKILL.md`. 5. Add regression tests covering backticks, `${...}`, backslashes, Unicode separators, closing script tags, and malformed HTML. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
templates/dashboard/index.html:98
Finding
DOM-Based Cross-Site Scripting in Dashboard List Rendering<![CDATA[ ## Vulnerability Details **File Location**: `templates/dashboard/index.html:98-102` **Vulnerability Type**: DOM-based cross-site scripting through unsafe `innerHTML` construction **Risk Level**: High ### Vulnerable Code ```javascript setList: (n, title, items) => { document.getElementById('list-' + n + '-title').textContent = title; document.getElementById('list-' + n).innerHTML = items.map(i => '<li><span>' + i.text + '</span><span class="badge ' + (i.status || '') + '">' + (i.badge || '') + '</span></li>' ).join(''); }, ``` ### Technical Analysis The `setList()` API concatenates `i.text`, `i.status`, and `i.badge` directly into an HTML string and assigns it to `innerHTML`. No output encoding, sanitization, or allowlist validation is performed. An attacker-controlled value can break out of the intended text or attribute context and introduce arbitrary HTML. Event-handler attributes, active elements, or other browser-supported execution mechanisms can consequently execute in the Canvas page. Using `textContent` for the list title does not protect the independently unsafe item fields. ### Attack Path 1. An attacker controls or influences data that the agent uses to populate a dashboard list. 2. A list property contains crafted HTML, such as an element with an event handler. 3. The agent calls `dashboard.setList()` with the malicious item. 4. `setList()` concatenates the value into a markup string. 5. Assignment to `innerHTML` causes the browser to parse the attacker-controlled markup. 6. The injected browser behavior executes when its activation condition is met. ### Impact Assessment The attacker can execute JavaScript in the dashboard's Canvas origin and manipulate the complete dashboard DOM. This can enable interface spoofing, alteration or theft of page-visible data, deceptive interaction prompts, and attempts to invoke the `openclaw://agent` deep-link handler. Host-level privileges depend on the Canvas sandbox and the behavior ...[truncated 29 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Construct list entries with DOM APIs and assign untrusted values through `textContent`: ```javascript setList: (n, title, items) => { const titleNode = document.getElementById('list-' + n + '-title'); const listNode = document.getElementById('list-' + n); titleNode.textContent = String(title); listNode.replaceChildren(); const allowedStatuses = new Set(['done', 'wip', 'todo']); for (const item of items) { const li = document.createElement('li'); const text = document.createElement('span'); const badge = document.createElement('span'); text.textContent = String(item.text ?? ''); badge.textContent = String(item.badge ?? ''); const status = allowedStatuses.has(item.status) ? item.status : ''; badge.classList.add('badge'); if (status) badge.classList.add(status); li.append(text, badge); listNode.appendChild(li); } } ``` Also validate the shape and maximum size of injected data. If HTML formatting is genuinely required, use a maintained allowlist-based sanitizer and disallow scripts, event attributes, dangerous URLs, and active embedded content. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
templates/tracker/index.html:126
Finding
DOM-Based Cross-Site Scripting in Tracker Item Rendering<![CDATA[ ## Vulnerability Details **File Location**: `templates/tracker/index.html:126-137` **Vulnerability Type**: DOM-based cross-site scripting through unsafe template interpolation **Risk Level**: High ### Vulnerable Code ```javascript function render() { const container = document.getElementById('items'); container.innerHTML = items.map((item, i) => ` <div class="item ${item.done ? 'done' : ''}" onclick="toggle(${i})"> <div class="checkbox">${item.done ? '' : ''}</div> <div class="item-content"> <div class="item-title">${item.title}</div> <div class="item-meta">${item.meta || ''}</div> </div> <div class="item-streak">${item.streak ? '🔥 ' + item.streak : ''}</div> </div> `).join(''); updateTimestamp(); } ``` ### Technical Analysis The tracker interpolates `item.title`, `item.meta`, and `item.streak` into an HTML template and assigns the result to `innerHTML`. These properties are accepted through the public `tracker.setItems()` and `tracker.addItem()` APIs without validation or encoding. A malicious item can inject elements and event-handler attributes into the generated document. The use of an inline `onclick` handler further mixes executable code and markup, making a strict Content Security Policy more difficult to deploy. ### Attack Path 1. An attacker causes malicious tracker data to be passed to `tracker.setItems()` or `tracker.addItem()`. 2. The payload is stored in the global `items` array. 3. The API invokes `render()`. 4. `render()` interpolates attacker-controlled properties into an HTML string. 5. The browser parses the string through `container.innerHTML`. 6. Injected active content executes immediately or when the user interacts with it. 7. The payload can manipulate the tracker or attempt to send deceptive commands through the agent deep-link mechanism. ### Impact Assessment Successful exploitation permits arbitrary JavaScript execution within the tracker page. An attacker can m ...[truncated 248 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Replace `innerHTML` rendering with explicit element construction: ```javascript function render() { const container = document.getElementById('items'); container.replaceChildren(); items.forEach((item, index) => { const row = document.createElement('div'); row.classList.add('item'); if (Boolean(item.done)) row.classList.add('done'); row.addEventListener('click', () => toggle(index)); const checkbox = document.createElement('div'); checkbox.className = 'checkbox'; const content = document.createElement('div'); content.className = 'item-content'; const title = document.createElement('div'); title.className = 'item-title'; title.textContent = String(item.title ?? ''); const meta = document.createElement('div'); meta.className = 'item-meta'; meta.textContent = String(item.meta ?? ''); const streak = document.createElement('div'); streak.className = 'item-streak'; streak.textContent = item.streak ? `🔥 ${String(item.streak)}` : ''; content.append(title, meta); row.append(checkbox, content, streak); container.appendChild(row); }); updateTimestamp(); } ``` Validate each item against a strict schema, constrain string lengths, and use `addEventListener()` instead of inline event handlers. Consider a restrictive Content Security Policy that disallows inline scripts. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
open-app.sh:23
Finding
Directory Traversal Can Expose Arbitrary Directories Over the Network<![CDATA[ ## Vulnerability Details **File Location**: `open-app.sh:7,23-28` **Vulnerability Type**: Directory traversal combined with excessive network exposure **Risk Level**: High ### Vulnerable Code ```bash APPS_DIR="${CANVAS_APPS_DIR:-$HOME/.openclaw/workspace/apps}" ``` ```bash # Check app exists if [ ! -d "$APPS_DIR/$APP_NAME" ]; then echo "❌ App not found: $APPS_DIR/$APP_NAME" exit 1 fi # Start server cd "$APPS_DIR/$APP_NAME" python3 -m http.server $PORT > /dev/null 2>&1 & ``` ### Technical Analysis `APP_NAME` is incorporated into a filesystem path without rejecting separators, `..` components, absolute paths, or symbolic-link escapes. The directory check only confirms that the resulting path exists; it does not verify that the canonical target remains inside `APPS_DIR`. In addition, `python3 -m http.server` binds to all available network interfaces by default unless `--bind` is specified. Although the application is described as a localhost service, the actual command can expose the selected directory to other hosts that can reach the machine. ### Attack Path 1. An attacker or untrusted caller supplies an app name containing traversal components, such as a sequence resolving outside the configured application root. 2. The concatenated path resolves to an existing directory outside `APPS_DIR`. 3. The directory check succeeds because it checks existence rather than containment. 4. The script changes into the unintended directory. 5. Python's HTTP server starts and publishes that directory on the selected port. 6. A network-adjacent host connects to the machine and retrieves files accessible to the server process. Symbolic links inside the application directory can provide an equivalent escape path if canonical containment is not enforced. ### Impact Assessment Files readable by the invoking user within the selected directory may become remotely accessible. Depending on the chosen path, exposed content could include workspace data, source co ...[truncated 253 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict app names to a safe single-component format: ```bash if [[ ! "$APP_NAME" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || [[ "$APP_NAME" == "." || "$APP_NAME" == ".." ]]; then echo "Invalid app name" >&2 exit 1 fi ``` 2. Resolve both paths canonically and enforce containment: ```bash BASE_DIR="$(cd "$APPS_DIR" && pwd -P)" APP_DIR="$(cd "$APPS_DIR/$APP_NAME" 2>/dev/null && pwd -P)" || exit 1 case "$APP_DIR/" in "$BASE_DIR"/*) ;; *) echo "App path escapes application directory" >&2; exit 1 ;; esac ``` 3. Bind the server exclusively to loopback: ```bash python3 -m http.server "$PORT" --bind 127.0.0.1 ``` 4. Validate that `PORT` is numeric and within the range `1-65535`. 5. Avoid following application-directory symbolic links unless explicitly intended. 6. Apply host firewall rules as defense in depth. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
close-app.sh:10
Finding
Predictable and Untrusted PID Files Permit Process Termination and File Clobbering<![CDATA[ ## Vulnerability Details **File Location**: `open-app.sh:46`, `close-app.sh:10-18` **Vulnerability Type**: Unsafe temporary-file handling and unvalidated process control **Risk Level**: Medium ### Vulnerable Code The server PID is written to a predictable shared temporary path: ```bash echo $SERVER_PID > "/tmp/canvas-app-$APP_NAME.pid" ``` The cleanup script later trusts that file without validating its ownership, type, contents, or associated process: ```bash PID_FILE="/tmp/canvas-app-$APP_NAME.pid" if [ -f "$PID_FILE" ]; then PID=$(cat "$PID_FILE") kill -9 $PID 2>/dev/null && echo "📡 Server stopped (PID: $PID)" rm "$PID_FILE" else echo "⚠️ No PID file found for $APP_NAME" fi ``` ### Technical Analysis The PID file resides in the globally shared `/tmp` namespace and has a predictable name influenced by `APP_NAME`. The open script writes through the path without checking whether it is a symbolic link, allowing a local attacker to pre-create the path as a link to another user-writable target. The close script accepts the file's contents as a process identifier and invokes `kill -9` without checking that the value is a valid positive integer or that the process is the Python server originally created by the application. Because `APP_NAME` is also not constrained to a safe filename component, path separators can affect where the PID file is read, written, or removed. ### Attack Path **Arbitrary same-user process termination:** 1. A local attacker predicts the PID filename for an application. 2. The attacker creates or modifies that file and stores the PID of another process owned by the same user. 3. The user invokes `close-app.sh` for the selected application. 4. The script reads the attacker-controlled PID. 5. `kill -9` terminates the unrelated process. **File clobbering:** 1. A local attacker creates the predictable PID path as a symbolic link to a file writable by the victim. 2. The user invokes `open-app.sh`. 3. Shell redirecti ...[truncated 492 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store runtime state in a private per-user directory rather than directly under `/tmp`: ```bash RUNTIME_DIR="${XDG_RUNTIME_DIR:-$HOME/.cache}/canvas-os" mkdir -p "$RUNTIME_DIR" chmod 700 "$RUNTIME_DIR" PID_FILE="$RUNTIME_DIR/$APP_NAME.pid" ``` 2. Enforce a strict allowlist for `APP_NAME`. 3. Create PID files atomically and reject symbolic links. 4. Verify that PID-file ownership and permissions match the current user. 5. Validate the contents before passing them to `kill`: ```bash [[ "$PID" =~ ^[1-9][0-9]*$ ]] || exit 1 ``` 6. Verify process identity using `/proc`, `ps`, or a random per-instance token before signaling it. 7. Attempt graceful `TERM` shutdown first, wait for exit, and use `KILL` only as a controlled fallback. 8. Quote process identifiers and use an end-of-options marker: ```bash kill -TERM -- "$PID" ``` ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
open-app.sh:17
Finding
Unconditional Force-Termination of Unrelated Port Owners<![CDATA[ ## Vulnerability Details **File Location**: `open-app.sh:17-18` **Additional Location**: `SKILL.md:94-99` **Vulnerability Type**: Unsafe process termination and denial of service **Risk Level**: Medium ### Vulnerable Code ```bash # Kill any existing server on this port lsof -ti:$PORT 2>/dev/null | xargs kill -9 2>/dev/null ``` The Skill instructions recommend the same behavior: ```bash # 1. Kill any existing server on the port lsof -ti:$PORT | xargs kill -9 2>/dev/null ``` ### Technical Analysis The script identifies every process associated with the selected port and immediately sends `SIGKILL`. It does not verify that the process belongs to Canvas OS, was launched by this project, or is safe to terminate. `SIGKILL` prevents applications from performing normal cleanup, flushing state, or saving data. A user-controlled `PORT` therefore becomes a mechanism for terminating unrelated processes owned by the invoking user. ### Attack Path 1. An unrelated application is listening on a known port. 2. An attacker or untrusted caller causes `open-app.sh` to be invoked with that port. 3. `lsof` returns the unrelated application's process identifier. 4. `xargs kill -9` immediately terminates the process. 5. The affected service becomes unavailable and may lose unsaved state. ### Impact Assessment Exploitation causes denial of service against processes that the invoking user is authorized to signal. It can terminate development servers, local databases, or other user applications and may cause data corruption due to abrupt shutdown. It does not bypass operating-system process ownership checks. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions Do not terminate arbitrary port owners. Refuse to start when the port is occupied: ```bash if lsof -tiTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then echo "Port $PORT is already in use" >&2 exit 1 fi ``` If automatic cleanup is required: 1. Terminate only a PID previously recorded by Canvas OS. 2. Verify that the PID still belongs to the expected Python HTTP server and application directory. 3. Send `SIGTERM` first and wait for a bounded interval. 4. Use `SIGKILL` only if the identity remains verified and graceful shutdown fails. 5. Validate the port as an integer in the range `1-65535`. 6. Remove the unconditional `lsof | kill -9` recommendation from `SKILL.md`. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (12)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The skill is presented as a UI/app-platform helper, but it also instructs arbitrary JavaScript injection into a live Canvas target using eval/document.write and direct node manipulation. That mismatch is dangerous because it hides powerful code-execution behavior behind an innocuous description, making abuse or unsafe invocation more likely.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The README explicitly promotes 'Live data injection via JavaScript eval' and 'Two-way communication' where apps send commands back to the agent, but it provides no warning about trust boundaries, input validation, or the system-impacting nature of those capabilities. In this skill's context, that omission is dangerous because combining eval-based injection with agent command callbacks can enable code execution, prompt/command injection, or unintended agent actions from app-controlled content.

Vague Triggers

Medium
Confidence
96% confidence
Finding
The README advertises highly generic trigger phrases such as 'Open business dashboard' and 'Show my stats on canvas' that are likely to collide with ordinary user requests. In an agentic environment, broad activation phrases can cause the skill to run unintentionally, which is especially risky here because the skill can launch local content, inject JavaScript, and receive commands back from the app.

Lp3

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding
The skill documents shell-capable operations such as starting HTTP servers, killing processes, and navigating/evaluating code on Canvas, but it declares no explicit tool scope or permissions boundary. This increases the risk of unintended command execution because users and the platform cannot clearly constrain what the skill is allowed to do.

Vague Triggers

Medium
Confidence
94% confidence
Finding
Trigger phrases like 'Open [app]', 'Update [element]', and 'Show [data] on canvas' are broad and overlap with normal conversational requests, increasing the chance the skill activates when the user did not intend privileged Canvas or shell actions. Because this skill can start servers and inject code into a target UI, accidental activation carries meaningful security risk.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The documented command `lsof -ti:$PORT | xargs kill -9` forcibly terminates any process bound to the port without validating ownership or warning the user. This can disrupt unrelated local services and cause data loss or denial of service if used on a shared or important port.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
This helper is explicitly designed to bypass normal Canvas loading restrictions by opening about:blank and then using an eval action to execute JavaScript that calls document.write() with attacker-controlled HTML. If html_content is influenced by untrusted input, this becomes arbitrary script/HTML injection in the Canvas target, enabling XSS-like behavior, credential theft, UI spoofing, or execution of privileged in-canvas actions.

Intent-Code Divergence

Medium
Confidence
96% confidence
Finding
The function is documented as HTML injection, but it actually returns an eval action that executes JavaScript in the target context. That mismatch is dangerous because downstream users may believe they are only rendering markup, while in reality they are enabling code execution with any scripts or event handlers embedded in the supplied HTML.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The script reads a PID from a predictable file in /tmp and then unconditionally sends SIGKILL to that PID before deleting the file. If the PID file is stale, tampered with, or points to a reused PID, the script can terminate the wrong process, creating a local denial-of-service risk and making recovery/debugging harder because SIGKILL bypasses graceful shutdown.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The script unconditionally force-kills every process bound to the requested port using `lsof ... | xargs kill -9`, with no validation, confirmation, or ownership check. Because the port is user-controlled, this can terminate unrelated services on the host, causing denial of service or data loss if a critical process is using that port.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The page sends user interaction data to an external agent via the custom openclaw://agent URI whenever a tracker item is toggled, and the add button also triggers an agent message. This creates an undisclosed data flow from local UI actions to another component, potentially exposing habit names and completion status without clear consent or visibility to the user.

Natural-Language Policy Violations

Low
Confidence
93% confidence
Finding
The document sets `lang="en"`, which hard-codes English as the page language. Under the stated policy, forcing a specific language without user opt-in or clear justification is a natural-language locale policy violation.

Static analysis

No suspicious patterns detected.