T09 · Insecure Skill Coding Practices
- Location
assets/https-server.py:57- Finding
Transcription Authentication Fails Open When Gateway Configuration Cannot Be Loaded
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill is mostly coherent for adding voice input, but it has real security weaknesses around microphone audio, gateway token handling, LAN exposure, and persistent UI modification that deserve review before installation.
Install only after reviewing the scripts and preferably use the newer split replacement skills. Keep the proxy bound to 127.0.0.1, avoid custom HTTPS ports unless the endpoint bug is fixed, do not expose it on a LAN without reliable authentication, and be aware that continuous mode can send transcribed speech into chat automatically after a short review period.
assets/https-server.py:57Transcription Authentication Fails Open When Gateway Configuration Cannot Be Loaded
assets/voice-input.js:28Custom HTTPS Ports Can Send Audio and the Gateway Token to a Client-Side Loopback Service
scripts/uninstall.sh:48Uninstaller Removes Unrelated Gateway Allowed Origins by Port Substring
VOICE_TRANSCRIBE_URL is taken from the environment and used directly in urllib.request.urlopen, allowing the service to forward arbitrary request bodies to any configured destination. In environments where attackers can influence configuration, this becomes an SSRF-style primitive and can exfiltrate microphone audio or probe internal network services; the risk is amplified because the proxy may intentionally be exposed beyond localhost.
loop = asyncio.get_event_loop()
data = await loop.run_in_executor(
None,
lambda: urllib.request.urlopen(req, timeout=120).read(),
)
return web.Response(
body=data,
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).
| 4.1 | `https-server.py` | **Path traversal protection**: `os.path.normpath()` + prefix check ensures static file serving stays within `CONTROL_UI` dir | Info | ✅ OK |
| 4.2 | `https-server.py` | Returns `HTTPForbidden` for traversal attempts | Info | ✅ OK |
| 4.3 | `deploy.sh` | Writes only to user-owned dirs (`~/.openclaw/`, `~/.config/systemd/user/`, npm global) | Info | ✅ OK |
| 4.4 | `deploy.sh` | No `chmod 777`, no world-writable files | Info | ✅ OK |
| 4.5 | `https-server.py` | TLS key now gets explicit `os.chmod(key_path, 0o600)` after cert generation | **Low** | ✅ Fixed |
**Fixed (4.5):** Explicit `os.chmod(key_path, 0o600)` is now applied after cert generation.
The skill’s declared purpose understates that it installs a persistent startup hook and performs UI patch/injection on gateway startup. Hidden or under-declared persistence and injection behavior is dangerous because it survives updates and continues modifying the interface automatically, which increases the chance of unnoticed long-term tampering.
The skill’s declared purpose understates that it installs a persistent startup hook and performs UI patch/injection on gateway startup. Hidden or under-declared persistence and injection behavior is dangerous because it survives updates and continues modifying the interface automatically, which increases the chance of unnoticed long-term tampering.
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).
echo "[1/5] Stopping and removing openclaw-voice-https.service..."
systemctl --user stop openclaw-voice-https.service 2>/dev/null || true
systemctl --user disable openclaw-voice-https.service 2>/dev/null || true
rm -f "$HOME/.config/systemd/user/openclaw-voice-https.service"
systemctl --user daemon-reload 2>/dev/null || true
echo " done."
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).
# 3) Remove voice-input.js from Control UI and undo index.html injection
echo "[3/5] Reverting Control UI..."
if [[ -f "$ASSET_DIR/voice-input.js" ]]; then
rm -f "$ASSET_DIR/voice-input.js"
echo " removed: voice-input.js asset"
fi
The script deletes files under $VOICE_DIR, which is derived from the WORKSPACE environment variable, without validating that the resolved path is the intended application workspace. If an attacker or confused operator sets WORKSPACE to an unexpected location, the uninstall script could remove arbitrary matching files and potentially rmdir that directory if it becomes empty.
# 7) Remove workspace runtime files (voice-input.js, https-server.py, i18n.json)
echo "[7/7] Removing workspace runtime files..."
for f in voice-input.js https-server.py i18n.json; do
rm -f "$VOICE_DIR/$f"
done
# Remove dir if empty
rmdir "$VOICE_DIR" 2>/dev/null && echo " removed: $VOICE_DIR" || echo " $VOICE_DIR not empty, kept."
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
| 4.1 | `https-server.py` | **Path traversal protection**: `os.path.normpath()` + prefix check ensures static file serving stays within `CONTROL_UI` dir | Info | ✅ OK |
| 4.2 | `https-server.py` | Returns `HTTPForbidden` for traversal attempts | Info | ✅ OK |
| 4.3 | `deploy.sh` | Writes only to user-owned dirs (`~/.openclaw/`, `~/.config/systemd/user/`, npm global) | Info | ✅ OK |
| 4.4 | `deploy.sh` | No `chmod 777`, no world-writable files | Info | ✅ OK |
| 4.5 | `https-server.py` | TLS key now gets explicit `os.chmod(key_path, 0o600)` after cert generation | **Low** | ✅ Fixed |
**Fixed (4.5):** Explicit `os.chmod(key_path, 0o600)` is now applied after cert generation.
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.
| 4.1 | `https-server.py` | **Path traversal protection**: `os.path.normpath()` + prefix check ensures static file serving stays within `CONTROL_UI` dir | Info | ✅ OK |
| 4.2 | `https-server.py` | Returns `HTTPForbidden` for traversal attempts | Info | ✅ OK |
| 4.3 | `deploy.sh` | Writes only to user-owned dirs (`~/.openclaw/`, `~/.config/systemd/user/`, npm global) | Info | ✅ OK |
| 4.4 | `deploy.sh` | No `chmod 777`, no world-writable files | Info | ✅ OK |
| 4.5 | `https-server.py` | TLS key now gets explicit `os.chmod(key_path, 0o600)` after cert generation | **Low** | ✅ Fixed |
**Fixed (4.5):** Explicit `os.chmod(key_path, 0o600)` is now applied after cert generation.
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
### All Medium/Low Issues Resolved
1. **Auth on `/transcribe` (3.2/3.3)** — ✅ Fixed. Bearer token auth matching gateway token.
2. **TLS key permissions (4.5)** — ✅ Fixed. Explicit `chmod 600` after generation.
3. **aiohttp version (8.1)** — ✅ Fixed. Minimum `>=3.9.0` documented and enforced.
### Remaining Recommendations
The skill declares substantial capabilities—file modification, shell execution, network access, environment use, and persistence via systemd/startup hooks—without an explicit tool-scope or permissions declaration. That omission weakens reviewability and informed consent, making it easier for users to run a skill that performs broader actions than expected.
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.
persistence:
- "User systemd service: openclaw-voice-https.service (HTTPS/WSS proxy)"
- "Gateway startup hook: voice-input-inject (re-injects JS after updates)"
privileges: user-level only, no root/sudo required
dependencies:
- python3 with aiohttp >= 3.9.0 (pip)
- faster-whisper transcription service on port 18790
The skill installs a user systemd service that starts on boot and a hook that re-injects UI changes after updates, creating durable persistence. Persistence is not inherently malicious, but in combination with script injection and network proxying it increases the blast radius of mistakes or compromise by keeping the behavior active without repeated user action.
- **Localhost by default**: The HTTPS proxy binds to `127.0.0.1` only. It is **not** reachable from other devices on your network unless you explicitly set `VOICE_HOST` to a LAN IP.
- **LAN exposure**: Setting `VOICE_HOST=<LAN-IP>` exposes the proxy (and by extension the gateway WebSocket and transcription endpoint) to all devices on that network. Only do this on trusted networks.
- **Persistence**: This skill installs a **user systemd service** (`openclaw-voice-https.service`) that starts automatically on boot, and a **gateway hook** that re-injects the UI script after updates. Use `uninstall.sh` to fully revert.
- **Self-signed TLS**: The auto-generated certificate is not trusted by browsers. You will see a certificate warning on first access.
## Deploy
This documented creation and enabling of a persistent HTTPS proxy service confirms long-lived background execution. In this skill’s context, persistence is more sensitive because the service fronts gateway WebSocket access and transcription functionality, and can optionally expose them to a LAN if configured broadly.
| Control UI HTML | `<npm-global>/openclaw/dist/control-ui/index.html` | Adds `<script>` tag for voice-input.js |
| Control UI asset | `<npm-global>/openclaw/dist/control-ui/assets/voice-input.js` | Copies mic button JS |
| Gateway config | `~/.openclaw/openclaw.json` | Adds HTTPS origin to `gateway.controlUi.allowedOrigins` |
| Systemd service | `~/.config/systemd/user/openclaw-voice-https.service` | Creates + enables persistent HTTPS proxy |
| Gateway hook | `~/.openclaw/hooks/voice-input-inject/` | Installs startup hook that re-injects JS after updates |
| Workspace files | `~/.openclaw/workspace/voice-input/` | Copies voice-input.js, https-server.py |
| TLS certs | `~/.openclaw/workspace/voice-input/certs/` | Auto-generated self-signed cert on first run |
The live transcription control states that captured speech is auto-sent after a short review period, but the description does not foreground this as a privacy-sensitive behavior. Users may reasonably expect transcription preview without automatic submission, leading to accidental disclosure of sensitive spoken content into chat workflows.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
return explicit
try:
# Safe: fixed argument list, no user input, no shell=True
npm_root = subprocess.check_output(["npm", "-g", "root"], text=True, stderr=subprocess.DEVNULL).strip()
candidate = os.path.join(npm_root, "openclaw", "dist", "control-ui")
if os.path.isdir(candidate):
return candidate
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
return
# Safe: fixed argument list, no user input, no shell=True
subprocess.run([
"openssl", "req", "-x509", "-nodes", "-newkey", "rsa:2048",
"-keyout", str(key_path),
"-out", str(cert_path),
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
return
# Safe: fixed argument list, no user input, no shell=True
subprocess.run([
"openssl", "req", "-x509", "-nodes", "-newkey", "rsa:2048",
"-keyout", str(key_path),
"-out", str(cert_path),
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.
"-subj", "/CN=openclaw-voice-local",
], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# SECURITY: Ensure private key is not group/world-readable
os.chmod(str(key_path), 0o600)
The code reads a bearer token from localStorage and reuses it for transcription requests, extending the token's scope to a secondary endpoint without clear need or user awareness. This increases blast radius if the transcription service is compromised, misconfigured, or reachable in an unexpected context, and localStorage storage also makes the token broadly accessible to any script running in the page.
The skill captures microphone audio and sends it to a transcription endpoint with fetch(), but the code provides no explicit consent or disclosure that recorded audio leaves the browser. Users may assume audio is processed locally or only used transiently for UI behavior, making this a meaningful privacy and transparency issue.
Continuous mode aggregates ongoing speech, periodically transcribes it, and by default progresses toward automatic submission unless interrupted. This creates a compounded privacy and integrity risk because larger amounts of potentially sensitive speech are collected and transformed into outbound chat content with limited user control.
The skill goes beyond a passive voice UI by programmatically inserting transcribed content into the chat textarea and automatically sending messages via handleSendChat(). In continuous mode it can submit speech-derived content after a short timeout, which creates an integrity and privacy risk because users may not realize spoken audio will become outbound chat messages without an explicit confirmation step.
Continuous transcription mode auto-sends recognized text after a short review timer unless the user interacts with the textarea. That behavior can transmit unintended, misrecognized, or sensitive content without a clear warning, especially in a voice feature where recognition errors are expected.
The startup hook unconditionally executes a host shell script via bash during gateway startup, which creates a privileged code-execution path with side effects outside the TypeScript runtime. Although the script path is not user-controlled and shell interpolation is avoided, this still expands the skill's trust boundary and allows any compromise or unexpected change to inject.sh to run arbitrary commands on the host.
Detected: suspicious.dangerous_exec