T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/config_server.py:91- Finding
Unauthenticated Cross-Origin OpenClaw Configuration Modification
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
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.
scripts/config_server.py:91Unauthenticated Cross-Origin OpenClaw Configuration Modification
assets/picker.html:270Script and DOM Injection Through Unescaped Model and Configuration Data
选择器已过期,请重新触发「选择模型」
') 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 'scripts/config_server.py:80Configuration Metadata Disclosure and Indefinite Extension of Local Service Lifetime
该代码块的主要功能是“获取并整理模型数据 + 读取当前配置状态 + 输出 JSON”,属于模型选择器的数据提供脚本。它确实与声明中的“从 OpenRouter 拉取模型、按厂商分组、按能力标注”部分一致,也为 UI 提供当前配置上下文。但声明强调的是完整的交互式模型管理器:用户可启用模型、选择主模型、应用后热更新配置并自动刷新页面。当前代码并未写入配置、未执行热更新、未刷新页面,也没有任何 Canvas UI 或交互逻辑。因此就这段代码本身而言,声明描述了一个更完整、更强的技能能力,而实际代码只实现了其中的数据获取/整理子集。此外,代码还访问了本地 openclaw CLI 读取配置,这一行为未在声明中明确提及。综合来看,描述与此代码块的实际行为存在实质性不匹配。
声明描述的是一个完整的模型管理/选择器技能,包含远程拉取模型、交互式界面、过滤分组、用户触发词以及应用后自动刷新等多个行为;而提供的代码片段只负责把外部传入的模型选择结果写入 OpenClaw 配置并进行热重载。这部分确实与“热更新配置”相关,但仅覆盖声明中的一小部分。由于核心宣称功能(拉取模型列表、UI 交互、筛选搜索、页面刷新)均未在代码中体现,代码实际行为与声明的主要用途存在实质性不匹配。
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.
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.
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.
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.
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.
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');
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.
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');
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.
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 calls execute external commands. Without careful input validation, this enables command injection.
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 calls execute external commands. Without careful input validation, this enables command injection.
def get_current_models():
try:
r = subprocess.run(
["openclaw", "config", "get", "agents.defaults.models"],
capture_output=True, text=True, timeout=10
)
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.
}
}
result = subprocess.run(
['openclaw', 'config', 'patch', '--stdin'],
input=json.dumps(patch), capture_output=True, text=True
)
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.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
['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 calls execute external commands. Without careful input validation, this enables command injection.
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 calls execute external commands. Without careful input validation, this enables command injection.
def patch_config(patch_obj):
patch_json = json.dumps(patch_obj)
result = subprocess.run(
["openclaw", "config", "patch", "--stdin"],
input=patch_json,
capture_output=True,
文件中的触发词和用户可见提示均以中文固定呈现,没有说明是否支持其他语言或允许用户按偏好选择语言。这可能构成语言/locale 约束,但文档中未给出用户 opt-in 或区域限定的理由。
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.
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.
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.
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.
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.
Detected: suspicious.generated_source_template_injection