T09 · Insecure Skill Coding Practices
- Location
scripts/runninghub_client.py:29- Finding
RunningHub API Key Stored in Plaintext Without Restrictive File Permissions
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill mostly does what it says, but it also ships an undisclosed Chrome DevTools automation script that can control a local browser session.
Install only if you are comfortable reviewing or removing the Chrome automation script first. Prefer RUNNINGHUB_API_KEY over --save-key, avoid putting API keys on the command line, treat uploaded images as sent to RunningHub, and do not run chrome_automation.py against a normal logged-in browser profile.
scripts/runninghub_client.py:29RunningHub API Key Stored in Plaintext Without Restrictive File Permissions
There is a serious description-behavior mismatch: the skill claims to perform straightforward API-based workflow execution, but static analysis indicates browser DevTools automation, localhost debugging access on 127.0.0.1:9222, raw socket use, and arbitrary JavaScript execution in a browser context. That hidden capability can be used to inspect browser state, access authenticated sessions, or manipulate local browser pages in ways unrelated to the declared purpose.
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
from runninghub_client import RunningHubClient, load_config, get_api_key
# Get API key from config
api_key = get_api_key()
# Initialize client
The skill requests or implies capabilities for environment access, local file read/write, and network access without declaring any explicit tool scope or permission boundaries. In an agent setting, this increases the chance of overbroad execution and makes it harder to review or constrain what the skill can do, especially since it handles API keys and local files.
The skill instructs users to persist an API key in a local config file but does not describe file permissions, storage protections, rotation, or multi-user access risks. Storing long-lived credentials in plaintext local configuration can expose them to other local processes, users, backups, or accidental commits.
The skill encourages uploading arbitrary local images to a third-party cloud service without clearly warning that local data will leave the host and be stored or processed remotely. This can lead to unintended disclosure of sensitive images, embedded metadata, or proprietary content.
The skill claims to execute RunningHub ComfyUI workflows via API, but this script instead drives a local Chrome instance through the DevTools debugging interface. That materially expands capability from API usage into browser/session control, allowing interaction with logged-in pages and local user context in ways the stated skill purpose does not justify.
The script creates pages in a local Chrome instance, connects to the DevTools socket, enables privileged domains, and executes JavaScript in-page. For an API-oriented skill, this is unjustified high-privilege behavior that can access session-backed web content and manipulate a user's browser beyond what is needed to submit cloud workflows.
The script immediately creates a new Chrome page through the local DevTools endpoint and attaches to the browser without any confirmation, warning, or runtime consent. Because DevTools access can affect a user's active browser session and logged-in state, silent automation raises meaningful security and privacy concerns even if intended for convenience.
This code sends HTTP requests to a localhost Chrome DevTools service, which is an internal network target with powerful browser-control capabilities. Although the destination is fixed to 127.0.0.1 rather than attacker-supplied, internal-service interaction is still security-relevant because it can be used to access and manipulate a local browser session outside the skill's stated API scope.
print("[1/5] Creating workflow page...")
import requests
resp = requests.put(f"http://127.0.0.1:9222/json/new?{WORKFLOW_URL}")
page_info = resp.json()
page_id = page_info.get("id")
ws_url = page_info.get("webSocketDebuggerUrl")
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
key = base64.b64encode(bytes([0x41] * 16)).decode()
handshake = f"GET {path} HTTP/1.1\r\nHost: {host}:{port}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
sock.send(handshake.encode())
response = sock.recv(1024)
if b"101" not in response:
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
print("✅ WebSocket connected successfully")
print("\n[4/5] Enabling Chrome DevTools domains...")
sock.send(send_ws_command(ws_url, "Runtime.enable", {}, 1))
sock.send(send_ws_command(ws_url, "DOM.enable", {}, 2))
sock.send(send_ws_command(ws_url, "Page.enable", {}, 3))
time.sleep(1)
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
print("\n[4/5] Enabling Chrome DevTools domains...")
sock.send(send_ws_command(ws_url, "Runtime.enable", {}, 1))
sock.send(send_ws_command(ws_url, "DOM.enable", {}, 2))
sock.send(send_ws_command(ws_url, "Page.enable", {}, 3))
time.sleep(1)
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
print("\n[4/5] Enabling Chrome DevTools domains...")
sock.send(send_ws_command(ws_url, "Runtime.enable", {}, 1))
sock.send(send_ws_command(ws_url, "DOM.enable", {}, 2))
sock.send(send_ws_command(ws_url, "Page.enable", {}, 3))
time.sleep(1)
while True:
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
})()
"""
sock.send(
send_ws_command(
ws_url,
"Runtime.evaluate",
The cleanup request also targets the internal localhost DevTools service, continuing unauthorized interaction with a powerful local control plane. While less severe than page creation and script execution, it still reflects out-of-scope access to internal browser-debugging infrastructure and could impact user browsing context if misused.
sock.close()
requests.get(f"http://127.0.0.1:9222/json/close/{page_id}")
print("\n" + "=" * 70)
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
sock.close()
requests.get(f"http://127.0.0.1:9222/json/close/{page_id}")
print("\n" + "=" * 70)
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
print("=" * 70)
for attempt in range(max_attempts):
resp = requests.post(
query_url, headers=headers, json={"taskId": task_id}, timeout=10
)
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
print("=" * 70)
try:
resp = requests.post(url, headers=headers, json=payload, timeout=10)
result = resp.json()
if result.get("taskId"):
The client uploads arbitrary local image files to a remote third-party service without an explicit privacy or data-transmission warning at the point of upload. Users may unintentionally transmit sensitive local content, especially because the feature operates directly on filesystem paths and returns externally hosted URLs.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
}
try:
response = requests.post(
url, headers=self.headers, json=payload, timeout=30
)
return response.json()
The script saves the API key to a local config file and only prints a success message, without warning that the credential will persist on disk. In shared environments or developer workstations, plaintext credential storage can lead to accidental disclosure through backups, repository inclusion, or local compromise.
The module docstring states the script is 'For automated image upload and workflow execution.' However, later output states that due to browser security restrictions it 'cannot directly upload local files via CDP,' and the implemented JavaScript only checks for file inputs/buttons rather than performing an upload. This is an active contradiction between the file’s stated intent and its actual behavior.
The script emits all primary user-facing status and usage text in English, including the workflow banner and option descriptions, without any opt-in or locale selection. Under the stated policy, forcing a specific language without user choice is a natural-language policy violation.
The manifest describes a client for submitting ComfyUI workflows, checking status, and retrieving results from RunningHub. In addition to those API operations, the code implements local credential storage in config.json via save_config(), which is a broader behavior not mentioned in the manifest description.
The manifest focuses on executing workflows, submitting tasks, querying status, and retrieving results. The code additionally reads local files and uploads them to RunningHub as media inputs, which is an extra capability not explicitly described at the manifest level.
No suspicious patterns detected.