Back to skill

Security audit

Skills Browser

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent local skill browser, but it exposes local skill contents too broadly and has unsafe server and launcher behavior that should be reviewed before use.

Review or patch this before installing. At minimum, bind the server to 127.0.0.1, remove wildcard CORS, validate skill IDs against enumerated directories, sanitize rendered Markdown or disable raw HTML, and replace the port-kill behavior with a safe conflict message or verified graceful shutdown.

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

T09 · Insecure Skill Coding Practices

Error
Location
server/server.py:82
Finding
Local Skill Data Exposed Through All-Interface Binding and Permissive CORS<![CDATA[ ## Vulnerability Details **File Location**: `server/server.py:82-89, 96-101, 119-124` **Vulnerability Type**: Unauthenticated network exposure and overly permissive cross-origin access **Risk Level**: High ### Complete Code Snippet ```python def do_OPTIONS(self): self.send_response(200) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type") self.end_headers() def do_GET(self): parsed = urlparse(self.path) if parsed.path == "/api/skills": self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(get_all_skills()).encode()) return ``` ```python def main(): port = 8765 import webbrowser webbrowser.open(f"http://localhost:{port}/") server = HTTPServer(("0.0.0.0", port), Handler) print("Skills Browser: http://127.0.0.1:" + str(port)) server.serve_forever() ``` ### Technical Analysis The Skill documentation describes access through `127.0.0.1`, which implies a local-only service. The implementation instead binds the HTTP server to `0.0.0.0`, making it listen on every available network interface. The API has no authentication or session authorization. It also returns `Access-Control-Allow-Origin: *`, allowing arbitrary web origins to request and read API responses where browser networking policy permits. These permissions are unnecessary for a local Skill browser whose frontend and API share the same origin. The API exposes Skill metadata and complete `SKILL.md` contents. Consequently, the network exposure and wildcard CORS exceed the minimum privileges required for the declared local browsing functionality. ### Attack Path 1. A user launches the Skill. 2. The HTTP server begins listening on port 8765 on al ...[truncated 975 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Bind the server exclusively to the loopback interface: ```python server = HTTPServer(("127.0.0.1", port), Handler) ``` 2. Remove `Access-Control-Allow-Origin: *`. The bundled frontend and API use the same origin and do not require CORS. 3. If cross-origin access is genuinely required, allow only an explicit trusted origin and validate the `Origin` header. 4. Validate the `Host` header to reduce DNS-rebinding exposure. 5. Consider generating a random per-launch authorization token and requiring it for API requests. 6. Add response security headers, including a restrictive Content Security Policy. 7. Document the actual network binding and security boundary accurately. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
server/server.py:43
Finding
Path Traversal in Skill Detail API<![CDATA[ ## Vulnerability Details **File Location**: `server/server.py:43-45, 92-101` **Vulnerability Type**: Directory traversal and unauthorized file read **Risk Level**: High ### Complete Code Snippet ```python def get_skill_detail(skill_id): md_path = os.path.join(SKILL_DIR, skill_id, "SKILL.md") if os.path.exists(md_path): with open(md_path, "r", encoding="utf-8") as f: content = f.read() ``` ```python if parsed.path.startswith("/api/skill/"): skill_id = parsed.path[11:] detail = get_skill_detail(skill_id) if detail: self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(detail).encode()) else: self.send_response(404) self.end_headers() return ``` ### Technical Analysis The server extracts `skill_id` directly from the request path and passes it to `os.path.join`. It does not require the identifier to be a single directory name, reject path separators, or verify that the normalized path remains inside `SKILL_DIR`. An identifier containing traversal components such as `..` can therefore cause the resolved path to escape the intended Skill directory. The hardcoded `SKILL.md` suffix limits the target filename, but it does not preserve the required directory boundary. The endpoint is unauthenticated and the service listens on all network interfaces, increasing the reachable attack surface. ### Attack Path 1. The victim starts the Skill browser. 2. An attacker connects to the exposed service. 3. The attacker submits a raw HTTP request to `/api/skill/` with one or more traversal components in the identifier. 4. `os.path.join(SKILL_DIR, skill_id, "SKILL.md")` resolves to a location outside the intended Skill root. 5. If an accessible `SKILL.md` exists at the resulting path, the server reads it. 6. The file contents are returned to ...[truncated 517 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Accept only known Skill identifiers returned by the server's own enumeration process. 2. Reject identifiers containing `/`, `\`, `.`, `..`, URL path delimiters, or null characters. 3. Resolve and validate the final path before opening it: ```python def get_skill_detail(skill_id): if not skill_id or skill_id in {".", ".."}: return None if os.path.basename(skill_id) != skill_id: return None root = os.path.realpath(SKILL_DIR) md_path = os.path.realpath(os.path.join(root, skill_id, "SKILL.md")) if os.path.commonpath([root, md_path]) != root: return None if not os.path.isfile(md_path): return None with open(md_path, "r", encoding="utf-8") as f: content = f.read() ``` 4. Account for symbolic links by validating the fully resolved path. 5. Return a generic `400 Bad Request` for malformed identifiers. 6. Combine path validation with loopback-only binding and API authorization. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
server/index.html:634
Finding
Stored DOM Cross-Site Scripting Through Unsanitized Markdown<![CDATA[ ## Vulnerability Details **File Location**: `server/index.html:634-650, 719-734` **Vulnerability Type**: Stored DOM-based cross-site scripting **Risk Level**: High ### Complete Code Snippet ```javascript function renderMarkdown(text) { if (!text) return ""; console.log("[Markdown] markedLoaded:", window.markedLoaded, "marked:", !!window.marked, "hljs:", typeof hljs); // Check whether marked.js has loaded and is available if (window.markedLoaded === false || !window.marked) { console.log("[Markdown] Using fallback parser"); return '<div data-markdown-parser="fallback">' + fallbackMarkdownParser(text) + '</div>'; } try { // Use the configured marked.js parser const result = window.marked.parse(text); console.log("[Markdown] Result length:", result.length, "Has hljs class:", result.includes('hljs')); return '<div data-markdown-parser="marked.js">' + result + '</div>'; } catch (e) { console.error("[Markdown] Error with marked.js:", e); return '<div data-markdown-parser="fallback">' + fallbackMarkdownParser(text) + '</div>'; } } ``` ```javascript const resp = await fetch(API_BASE + "/api/skill/" + skillId); const data = await resp.json(); detailPanel.innerHTML = '<div class="detail-header">' + '<div class="detail-title-container">' + '<div class="detail-title">' + escapeHtml(data.frontmatter.name || skillId) + '</div>' + (data.frontmatter.BusinessSupportVersion ? '<div class="detail-version">v' + escapeHtml(data.frontmatter.BusinessSupportVersion) + '</div>' : '') + '</div>' + (data.frontmatter.description ? '<div class="detail-description">' + escapeHtml(data.frontmatter.description) + '</div>' : '') + '<div class="detail-meta">' + (data.frontmatter.Author ? '<div class="detail-meta-item"><span>Author</span> ' + escapeHtml(data.frontmatter.Author) + '</div>' : '') + ...[truncated 2218 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Sanitize all generated Markdown HTML with a maintained allowlist-based sanitizer before assigning it to `innerHTML`. 2. Configure the Markdown renderer to reject or escape raw HTML. 3. Allow only necessary formatting tags and attributes. Remove: - Event-handler attributes such as `onclick` and `onerror`. - `script`, `iframe`, `object`, `embed`, and similar active elements. - Dangerous URL schemes such as `javascript:`. 4. Prefer constructing DOM nodes through safe APIs when feasible. 5. Add a restrictive Content Security Policy, for example one that disallows inline scripts and restricts outbound connections. 6. Treat all sibling Skill documentation as untrusted input, even when it is stored locally. 7. Add regression tests using raw HTML, event-handler attributes, SVG payloads, and dangerous link protocols. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
skillsbrowser.sh:7
Finding
Launcher Forcefully Terminates Unrelated Processes on Port 8765<![CDATA[ ## Vulnerability Details **File Location**: `skillsbrowser.sh:7` **Vulnerability Type**: Unsafe process termination and least-privilege violation **Risk Level**: Medium ### Complete Code Snippet ```bash # Terminate any service that may be running on port 8765 lsof -ti:8765 | xargs kill -9 2>/dev/null ``` ### Technical Analysis The launcher enumerates every process using port 8765 and sends each process `SIGKILL`. It does not verify that a target process was previously started by this Skill, belongs to the expected user, or is an instance of `server.py`. `SIGKILL` prevents the target from running shutdown handlers, flushing state, or releasing resources cleanly. Terminating arbitrary port owners is unnecessary for the declared functionality and exceeds the minimum process-control authority required to start a local browser service. ### Attack Path 1. An unrelated application binds to port 8765. 2. The user runs `skillsbrowser.sh`. 3. `lsof -ti:8765` returns the unrelated application's process ID. 4. `xargs kill -9` immediately terminates that process. 5. The unrelated application cannot perform cleanup or save pending state. ### Impact Assessment The behavior can cause: - Denial of service against unrelated local applications. - Loss of unsaved work or in-memory state. - Data corruption where the terminated process was performing writes. - Unexpected termination of multiple processes if more than one process is associated with the port. The command operates with the launcher's user privileges. It does not independently escalate privileges, but it violates process isolation and least-privilege expectations. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not terminate arbitrary processes based only on port ownership. 2. If port 8765 is occupied, fail safely with a clear message or select an available ephemeral port. 3. Track the PID of the server instance launched by this Skill in a user-owned runtime file. 4. Before terminating a recorded PID, verify its owner and command line. 5. Use a graceful signal such as `SIGTERM` first and allow a bounded shutdown period. 6. Use `SIGKILL` only as a final fallback against a verified process belonging to this Skill. 7. Avoid suppressing all error output, because silent failures make unsafe process handling harder to diagnose. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • 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 (11)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The declared purpose and implementation evidence do not match: the skill claims a local HTML/Python browser, but analysis indicates undeclared Markdown parsing/rendering and code that does not clearly correspond to the stated UI stack. Such mismatches are risky because hidden or undeclared functionality can mask content injection, unsafe rendering, or other behavior that users and reviewers would not expect from a simple local browser.

Context-Inappropriate Capability

High
Confidence
99% confidence
Finding
The skill-detail endpoint uses user-controlled skill_id directly in os.path.join(SKILL_DIR, skill_id, "SKILL.md") without validation or canonicalization. An attacker can supply path traversal sequences such as ../ to escape the intended skills directory and read arbitrary files named SKILL.md elsewhere on disk, which exceeds the justified capability of a local browser for skills.

Lp3

Medium
Category
MCP Least Privilege
Confidence
84% confidence
Finding
The skill metadata declares no tool scope or permission boundaries even though the analyzed package is reported to include file read, network, and shell-capable behavior. Missing explicit scope is dangerous because it prevents reviewers and enforcement layers from understanding or constraining what the skill may do, increasing the risk of unexpected filesystem access, command execution, or local service exposure.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The document declares `lang="zh-CN"`, and the visible UI text throughout the page is Chinese-only, which indicates the skill interface is fixed to a specific language. Under the policy, locale constraints should either be optional for the user or clearly documented as region-specific and justified.

Session Persistence

Medium
Category
Rogue Agent
Content
className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[a]},{
begin:/'/,end:/'/,contains:[a]},{begin:/[^\s"'=<>`]+/}]}]}]};return{
name:"HTML, XML",
aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],
case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin:/<![a-z]/,
end:/>/,relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{
className:"meta",begin:/<![a-z]/,end:/>/,contains:[i,r,o,s]}]}]
Confidence
75% 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.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This endpoint serves skill metadata and full skill content from disk over HTTP with no authentication, authorization, or explicit user disclosure that local files are being exposed. In the context of a local skill browser this may seem expected, but it still creates an information exposure surface, especially when combined with network accessibility or embedding of sensitive data inside skill files.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The server binds to 0.0.0.0, making the browser accessible on all network interfaces rather than only the local machine, while automatically opening a browser as if it were a local-only tool. This expands the attack surface to other hosts on the network, making the file-exposure endpoints materially more dangerous in this skill context.

Context-Inappropriate Capability

Medium
Confidence
98% confidence
Finding
The script indiscriminately identifies any process bound to port 8765 and forcefully terminates it with kill -9 before starting its own server. This can kill unrelated local services, cause data loss or corruption by bypassing graceful shutdown, and creates a denial-of-service condition on the host beyond what is necessary for a simple skill browser.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
Using kill -9 through xargs on any PID returned for port 8765 performs an immediate, non-interactive termination with no notice to the user. Because SIGKILL prevents cleanup handlers from running, this can disrupt unrelated applications, lose unsaved state, and make the behavior surprising and unsafe in a local developer environment.

Natural-Language Policy Violations

Low
Confidence
84% confidence
Finding
The natural-language comments around the script's core behavior are written only in Chinese, which imposes a specific language without any opt-in or justification. This can violate language/locale policy expectations when the skill is intended for general use by a broader audience.

Missing User Warnings

Low
Confidence
86% confidence
Finding
Line L11 starts `python3 server.py` in the background, which is a subprocess execution with persistent effects, but the script gives no active output or warning to inform the user that a local server is being launched. The only nearby explanation is a comment in Chinese, which may not function as a clear disclosure for all users of the skill.

Static analysis

No suspicious patterns detected.