Back to skill

Security audit

Openrouter Model Picker

Security checks for vulnerabilities and agentic risk

Overview

The skill is a real OpenRouter model picker, but its local configuration server can be used without authentication to change OpenClaw model settings.

Review before installing. This skill can permanently change which OpenClaw models are enabled, selected as primary, or used as fallbacks. Only run it when you trust the local environment, avoid browsing untrusted sites while the picker server is running, and prefer a version that adds a per-session token, strict origin checks, server-side model validation, and safer HTML rendering.

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

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/config_server.py:91
Finding

Unauthenticated Cross-Origin OpenClaw Configuration Modification

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
assets/picker.html:270
Finding

Script and DOM Injection Through Unescaped Model and Configuration Data

Content
View full analysis
var MY_PICKER_TS = \"' + ts + '\";\nwindow.__PICKER_DATA__ = ' + data + ';\ninitWithData(window.__PICKER_DATA__);\n' html = html.replace('', inject + '') # 让旧的固定-ref picker 显示过期提示 stale_dir = '${OPENCLAW_CANVAS}/documents/model-picker' if os.path.exists(stale_dir): with open(os.path.join(stale_dir, 'index.html'), 'w') as f: f.write('

选择器已过期,请重新触发「选择模型」

') with open(html_path, 'w') as f: f.write(html) print('OK') " <<< "$DATA" ``` The UI concatenates model metadata and aliases into HTML strings: ```javascript function rowHtml(m){ var sel = selectedIds.has(m.id); var prim = m.id === document.getElementById('primarySel').value; var rc = 'model-row' + (sel ? ' selected' : ''); var ab = sel ? '-' : '+'; var alias = aliases[m.id]; var displayName = alias ? m.name + ' (' + alias + ')' : m.name; var primBadge = prim ? '主模型' : ''; var virtBadge = m._virtual ? '已配置' : ''; return '
Remediation
View remediation
` element and escape at least `<`, `>`, `&`, U+2028, and U+2029 before insertion. 4. Parse the JSON from the element's text content rather than constructing executable JavaScript. 5. Replace HTML-string concatenation with DOM construction: - Use `document.createElement`. - Assign untrusted visible values through `textContent`. - Assign attributes through `setAttribute` after validating their values. - Avoid constructing class names and element IDs from unrestricted external strings. 6. Validate model records against a strict schema. Require expected primitive types and reject unknown capability names or malformed identifiers. 7. If `innerHTML` cannot be eliminated, apply context-specific HTML and attribute encoding and use a reviewed sanitizer such as DOMPurify. Encoding is still preferable for plain text. 8. Validate `message` events by checking both `e.origin` and `e.source` against the expected trusted parent. 9. Do not use `"*"` as a `postMessage` target origin where a specific trusted origin is known. 10. Add a restrictive Content Security Policy that blocks inline script and event-handler execution. Move legitimate inline JavaScript into a separate static file or authorize it with a nonce. 11. Treat upstream API metadata and existing local configuration values as untrusted even when they normally originate from reputable components. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/config_server.py:80
Finding

Configuration Metadata Disclosure and Indefinite Extension of Local Service Lifetime

Content
View full analysis
0 and elapsed > IDLE_TIMEOUT: shutdown_reason = f"idle-{elapsed:.0f}s" break except KeyboardInterrupt: shutdown_reason = "interrupt" ``` ### Technical Ana ...[truncated 2249 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • 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
Findings (22)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

该代码块的主要功能是“获取并整理模型数据 + 读取当前配置状态 + 输出 JSON”,属于模型选择器的数据提供脚本。它确实与声明中的“从 OpenRouter 拉取模型、按厂商分组、按能力标注”部分一致,也为 UI 提供当前配置上下文。但声明强调的是完整的交互式模型管理器:用户可启用模型、选择主模型、应用后热更新配置并自动刷新页面。当前代码并未写入配置、未执行热更新、未刷新页面,也没有任何 Canvas UI 或交互逻辑。因此就这段代码本身而言,声明描述了一个更完整、更强的技能能力,而实际代码只实现了其中的数据获取/整理子集。此外,代码还访问了本地 openclaw CLI 读取配置,这一行为未在声明中明确提及。综合来看,描述与此代码块的实际行为存在实质性不匹配。

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

声明描述的是一个完整的模型管理/选择器技能,包含远程拉取模型、交互式界面、过滤分组、用户触发词以及应用后自动刷新等多个行为;而提供的代码片段只负责把外部传入的模型选择结果写入 OpenClaw 配置并进行热重载。这部分确实与“热更新配置”相关,但仅覆盖声明中的一小部分。由于核心宣称功能(拉取模型列表、UI 交互、筛选搜索、页面刷新)均未在代码中体现,代码实际行为与声明的主要用途存在实质性不匹配。

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding

The skill document instructs use of shell commands, network access, and file writes, but does not declare any tool scope or permission boundaries. This creates an over-privileged execution model where an agent may perform sensitive local configuration changes and network fetches without explicit user-visible constraints.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The trigger phrases include broad everyday expressions such as '换模型' and '管理模型', which can cause the skill to activate unintentionally during ordinary conversation. Because activation leads to network access, local file writes, server startup, and configuration changes, accidental invocation materially increases the risk of unauthorized or surprise system modification.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The description does not prominently warn that the skill will modify local configuration and apply hot updates immediately. Users may interpret it as a read-only model browser, so the lack of upfront disclosure undermines informed consent for a sensitive action that can alter runtime behavior without restart.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The skill is presented as a model picker, but the documentation states that hot updates cover all agents.* and models.* configuration. That broader mutation scope means a user invoking a seemingly narrow model-selection action could trigger changes to unrelated agent behavior or routing, increasing the blast radius of misuse or compromise.

Content

No source excerpt is available for this finding.

Internal Network Request

Medium
Category
Server-Side Request Forgery
Confidence
70% confidence
Finding

Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.

Content

Scanner excerpt · assets/picker.html (reported line 141)May include surrounding context.

html
function checkHealthNow(){
  console.log('[picker:health] pinging http://127.0.0.1:18790/health ...');
  fetch('http://127.0.0.1:18790/health')
    .then(function(r){
      console.log('[picker:health] response status:', r.status);
      if(!r.ok) throw new Error('gone');

Internal Network Request

Medium
Category
Server-Side Request Forgery
Confidence
70% confidence
Finding

Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.

Content

Scanner excerpt · assets/picker.html (reported line 422)May include surrounding context.

html
function checkHealthNow(){
  console.log('[picker:health] pinging http://127.0.0.1:18790/health ...');
  fetch('http://127.0.0.1:18790/health')
    .then(function(r){
      console.log('[picker:health] response status:', r.status);
      if(!r.ok) throw new Error('gone');

External Transmission

Medium
Category
Data Exfiltration
Confidence
91% confidence
Finding

The page sends configuration data to a localhost service without any visible authentication, origin binding, or anti-CSRF-style protection beyond a client-controlled timestamp. If this UI can be embedded or messaged by an unexpected parent, or if another local process exposes the same port, the request could modify local gateway configuration and force a reload of the parent page.

Content

Scanner excerpt · assets/picker.html (reported line 422)May include surrounding context.

html
console.log('[picker] apply', {primary:primary, count:enabled.length});
  btn.textContent = '应用中...';
  btn.disabled = true;
  fetch('http://127.0.0.1:18790/apply', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({ primary: primary, enabled: enabled, fallbacks: fallbacks, _picker_ts: MY_PICKER_TS })

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/config_server.py (reported line 60)May include surrounding context.

python
def get_picker_data():
    script = os.path.join(SCRIPT_DIR, "fetch_models.py")
    result = subprocess.run(
        [sys.executable, script],
        capture_output=True, text=True, timeout=60
    )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/config_server.py (reported line 70)May include surrounding context.

python
def get_current_models():
    try:
        r = subprocess.run(
            ["openclaw", "config", "get", "agents.defaults.models"],
            capture_output=True, text=True, timeout=10
        )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
88% confidence
Finding

Although the subprocess invocation itself is not shell-injectable, it applies configuration changes sourced directly from an unauthenticated local HTTP POST endpoint with permissive CORS. Any local process, or potentially a malicious website via the victim's browser, could send requests to enable, disable, or replace OpenClaw models, causing unauthorized configuration tampering and service disruption.

Content

Scanner excerpt · scripts/config_server.py (reported line 121)May include surrounding context.

python
}
        }

        result = subprocess.run(
            ['openclaw', 'config', 'patch', '--stdin'],
            input=json.dumps(patch), capture_output=True, text=True
        )

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The /apply path performs sensitive configuration mutation and writes a marker file without any authentication, authorization, origin restriction, or user-confirmation mechanism in this server. In the context of a model-management skill, this is more dangerous because changing model configuration directly affects which remote AI services are used, can disable models, and can be triggered by any local actor or browser-based cross-origin request to the loopback service.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/config_server.py (reported line 125)May include surrounding context.

python
['openclaw', 'config', 'patch', '--stdin'],
            input=json.dumps(patch), capture_output=True, text=True
        )
        validate = subprocess.run(
            ['openclaw', 'config', 'validate'],
            capture_output=True, text=True
        )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/fetch_models.py (reported line 58)May include surrounding context.

python
def load_current_config():
    """Use openclaw config get to read current model settings."""
    try:
        r = subprocess.run(
            ["openclaw", "config", "get", "agents.defaults"],
            capture_output=True, text=True, timeout=10
        )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/update_config.py (reported line 14)May include surrounding context.

python
def patch_config(patch_obj):
    patch_json = json.dumps(patch_obj)
    result = subprocess.run(
        ["openclaw", "config", "patch", "--stdin"],
        input=patch_json,
        capture_output=True,

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
81% confidence
Finding

文件中的触发词和用户可见提示均以中文固定呈现,没有说明是否支持其他语言或允许用户按偏好选择语言。这可能构成语言/locale 约束,但文档中未给出用户 opt-in 或区域限定的理由。

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Low
Category
Not specified by scanner
Confidence
77% confidence
Finding

Providing a direct command-line path to modify configuration bypasses the interactive UI safeguards and increases the chance of accidental or scripted misconfiguration. In a skill expected to be user-driven through Canvas, an alternate non-interactive write path expands the attack surface and weakens user awareness of impactful changes.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
96% confidence
Finding

The page declares lang="zh", and the visible UI text throughout the file is exclusively Chinese, which indicates the skill is designed around a fixed language experience. The policy allows locale constraints only when the user is given a choice or when the restriction is clearly documented and justified, neither of which is present in this file.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
89% confidence
Finding

The docstring states the server 'shuts down within 5 seconds' after a successful POST /apply. In implementation, successful apply only calls request_shutdown() at L153-L156, and the main loop exits on the next iteration after handle_request() returns at L188-L192, which is typically immediate after the response rather than a defined 5-second delay. This is an active documentation mismatch about lifecycle behavior.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
76% confidence
Finding

The virtual model display names embed Chinese text in parentheses, which imposes a specific locale in user-visible strings without any opt-in or alternative. This can violate language/locale policy when the skill does not document or justify the locale choice.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

This function makes an HTTP request to openrouter.ai to retrieve model metadata, but the code provides no confirmation prompt or user-facing notice at the point of transmission. Although the module docstring describes fetching models, there is no runtime disclosure that network access will occur.

Content

No source excerpt is available for this finding.

Static analysis

Detected: suspicious.generated_source_template_injection

User-controlled placeholder is embedded directly into generated source code.

Critical
Code
suspicious.generated_source_template_injection
Location
SKILL.md:71