T09 · Insecure Skill Coding Practices
- Location
ui/server.py:846- Finding
Unauthenticated cross-origin access to credential and configuration APIs
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill is a coherent OpenClaw model-management tool, but its local dashboard exposes unauthenticated credential and configuration-changing APIs that any browser origin can call while the server is running.
Review before installing. The CLI workflow appears purpose-aligned, but avoid running the dashboard server unless it has authentication and trusted-origin checks added. Remove legacy backup UI/server files from installed artifacts, restrict .env writes to known provider keys, and rotate provider or Telegram tokens if you ran this server while browsing untrusted sites.
ui/server.py:846Unauthenticated cross-origin access to credential and configuration APIs
ui/server_v1_backup.py:429Legacy backup server retains unauthenticated cross-origin credential mutation
Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.
File: switchboard.sh lines ~130-140
Severity: MEDIUM
The backup pruning logic uses ls -t | tail | xargs rm -f. If two switchboard commands run concurrently (e.g., two agents, or rapid CLI calls), both could backup and prune simultaneously, potentially deleting each other's backups or counting wrong.
Fix: Use a lockfile for backup operations:
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
4. **No direct JSON editing** — All config changes go through `openclaw models set` CLI. The skill never does raw JSON manipulation of model fields (except import, which validates first).
5. **Model format regex** — `validate_model_ref()` uses strict regex that blocks shell metacharacters, path traversal (`../`), and null bytes. A malicious model name like `anthropic/../../etc/passwd` is rejected by the regex.
6. **Provider diversity enforcement** — redundancy.py correctly ensures fallback chains don't stack the same provider.
Referenced artifact was not completely inspected
The Canvas UI at `ui/index.html` shows:
The prune pipeline uses ls | tail | xargs rm without null-delimited handling, which is unsafe for filenames containing whitespace, newlines, or leading dash characters. Although backup filenames created by this script are predictable, the directory is user-writable and a crafted matching file or symlink placed there could cause unintended deletions during cleanup.
local count
count=$(ls -1 "$BACKUP_DIR"/openclaw-*.json 2>/dev/null | wc -l | tr -d ' ')
if [ "$count" -gt "$MAX_BACKUPS" ]; then
ls -t "$BACKUP_DIR"/openclaw-*.json | tail -n +"$((MAX_BACKUPS + 1))" | xargs rm -f 2>/dev/null || true
log_dim "Pruned old backups (keeping last $MAX_BACKUPS)"
fi
) 9>"$LOCKFILE"
Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.
</div>
</div>
<!-- Model Picker Modal (for fix actions) -->
<div class="picker-overlay" id="pickerOverlay">
<div class="picker">
<h2 id="pickerTitle">Select Model</h2>
This UI sends raw API keys to a backend endpoint that persists them to a .env file, creating a concentrated secret store and increasing exposure if the local app, filesystem, backups, logs, or repo handling are weak. In skill context, this is more dangerous because the page is explicitly an admin-like credential manager, so compromise of the surrounding service could directly yield provider secrets for multiple AI services.
api('POST', '/api/key', { key: envVar, value: input.value.trim() })
.then(function(res) {
if (res.ok) {
showModalStatus('success', '✓ Saved ' + envVar + ' to .env');
toast('✓ ' + envVar + ' saved to .env', 'success');
setTimeout(function() {
closeModal();
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
.then(function(res) {
if (res.ok) {
showModalStatus('success', '✓ Saved ' + envVar + ' to .env');
toast('✓ ' + envVar + ' saved to .env', 'success');
setTimeout(function() {
closeModal();
loadStatus();
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
.then(function(res) {
if (res.ok) {
showModalStatus('success', '✓ Saved ' + envVar + ' to .env');
toast('✓ ' + envVar + ' saved to .env', 'success');
setTimeout(function() {
closeModal();
loadStatus();
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
.then(function(res) {
if (res.ok) {
showModalStatus('success', '✓ Saved ' + envVar + ' to .env');
toast('✓ ' + envVar + ' saved to .env', 'success');
setTimeout(function() {
closeModal();
loadStatus();
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
from pathlib import Path
PORT = int(os.environ.get("SWITCHBOARD_PORT", "8770"))
ENV_FILE = os.environ.get("SWITCHBOARD_ENV", os.path.expanduser("~/.openclaw/workspace/.env"))
REGISTRY_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "model-registry.json")
CONFIG_FILE = os.path.expanduser("~/.openclaw/openclaw.json")
BACKUP_DIR = os.path.expanduser("~/.openclaw/backups/switchboard")
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
from pathlib import Path
PORT = int(os.environ.get("SWITCHBOARD_PORT", "8770"))
ENV_FILE = os.environ.get("SWITCHBOARD_ENV", os.path.expanduser("~/.openclaw/workspace/.env"))
REGISTRY_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "model-registry.json")
CONFIG_FILE = os.path.expanduser("~/.openclaw/openclaw.json")
BACKUP_DIR = os.path.expanduser("~/.openclaw/backups/switchboard")
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
if __name__ == "__main__":
print(f"🔀 Model Switchboard server on http://127.0.0.1:{PORT}")
print(f" .env file: {ENV_FILE}")
print(f" Registry: {REGISTRY_FILE}")
print(f" Config: {CONFIG_FILE}")
server = http.server.HTTPServer(("127.0.0.1", PORT), Handler)
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
if __name__ == "__main__":
print(f"🔀 Model Switchboard server on http://127.0.0.1:{PORT}")
print(f" .env file: {ENV_FILE}")
print(f" Registry: {REGISTRY_FILE}")
print(f" Config: {CONFIG_FILE}")
server = http.server.HTTPServer(("127.0.0.1", PORT), Handler)
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.
### L-4: Backup permissions — umask not set
**File:** `switchboard.sh` line ~125
`chmod 600 "$backup_file"` is good, but the `mkdir -p "$BACKUP_DIR"` doesn't set directory permissions. On a multi-user system, the backup directory could be world-readable.
**Fix:** Add `chmod 700 "$BACKUP_DIR"` after mkdir.
### L-5: model-registry.json — `openai/dall-e-3` has empty safeRoles
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.
for pid, pinfo in registry.get("providers", {}).items():
auth_envs = pinfo.get("authEnv", [])
has_auth = False
auth_via = None
# Check environment variables
The introductory comments present a strong safety guarantee that the tool will never crash the gateway. In practice, functions such as set_primary, set_image, import_config, restore_backup, and redundancy_apply modify the active OpenClaw configuration, and the script merely checks status afterward or advises a restart, so the documentation overstates the absence of disruptive side effects.
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
# Ensure backup/log directory exists early
mkdir -p "$BACKUP_DIR" 2>/dev/null
chmod 700 "$BACKUP_DIR" 2>/dev/null
# ── Colors & Output ────────────────────────────────────────────
RED='\033[0;31m'
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
# Ensure backup/log directory exists early
mkdir -p "$BACKUP_DIR" 2>/dev/null
chmod 700 "$BACKUP_DIR" 2>/dev/null
# ── Colors & Output ────────────────────────────────────────────
RED='\033[0;31m'
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
# Ensure backup/log directory exists early
mkdir -p "$BACKUP_DIR" 2>/dev/null
chmod 700 "$BACKUP_DIR" 2>/dev/null
# ── Colors & Output ────────────────────────────────────────────
RED='\033[0;31m'
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
# Ensure backup/log directory exists early
mkdir -p "$BACKUP_DIR" 2>/dev/null
chmod 700 "$BACKUP_DIR" 2>/dev/null
# ── Colors & Output ────────────────────────────────────────────
RED='\033[0;31m'
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
fi
cp "$OPENCLAW_CONFIG" "$backup_file"
chmod 600 "$backup_file"
log_ok "Config backed up: ${DIM}$backup_file${NC}"
# Prune old backups (keep MAX_BACKUPS) with lockfile to prevent races
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
fi
cp "$OPENCLAW_CONFIG" "$backup_file"
chmod 600 "$backup_file"
log_ok "Config backed up: ${DIM}$backup_file${NC}"
# Prune old backups (keep MAX_BACKUPS) with lockfile to prevent races
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
fi
cp "$OPENCLAW_CONFIG" "$backup_file"
chmod 600 "$backup_file"
log_ok "Config backed up: ${DIM}$backup_file${NC}"
# Prune old backups (keep MAX_BACKUPS) with lockfile to prevent races
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
fi
cp "$OPENCLAW_CONFIG" "$backup_file"
chmod 600 "$backup_file"
log_ok "Config backed up: ${DIM}$backup_file${NC}"
# Prune old backups (keep MAX_BACKUPS) with lockfile to prevent races
Dynamic import() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.
"backupCount": backup_count,
"issues": issues,
"registryModels": list(registry.get("models", {}).keys()),
"timestamp": __import__("datetime").datetime.now().isoformat()
}
Detected: suspicious.install_untrusted_source