T09 · Insecure Skill Coding Practices
- Location
scripts/setup_device.py:35- Finding
Device-Controlled Path Traversal in Image Backup
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill mostly matches its Holocube-control purpose, but its setup tooling can delete device images and has an unsafe backup path that could overwrite local files if pointed at a malicious or compromised device.
Review before installing. Use it only with a Holocube you control, avoid pointing --ip at untrusted hosts, make an independent backup before setup, avoid onboard.py unless you are willing to lose existing device images, and provide Gemini credentials through the environment or config rather than a command-line argument.
scripts/setup_device.py:35Device-Controlled Path Traversal in Image Backup
scripts/generate_sprites.py:263Gemini API Key Can Be Exposed Through Command-Line Arguments
scripts/onboard.py:151Onboarding Unconditionally Deletes Existing Device Images
The mismatch here is especially risky because the skill reportedly includes backup and optional clearing/deletion of all device images without clearly declaring those destructive effects. In the context of an IoT display skill, undisclosed deletion or reset-adjacent file management can cause data loss, disrupt the device state, and make users underestimate the consequences of running setup commands.
The mismatch here is especially risky because the skill reportedly includes backup and optional clearing/deletion of all device images without clearly declaring those destructive effects. In the context of an IoT display skill, undisclosed deletion or reset-adjacent file management can cause data loss, disrupt the device state, and make users underestimate the consequences of running setup commands.
The mismatch here is especially risky because the skill reportedly includes backup and optional clearing/deletion of all device images without clearly declaring those destructive effects. In the context of an IoT display skill, undisclosed deletion or reset-adjacent file management can cause data loss, disrupt the device state, and make users underestimate the consequences of running setup commands.
Copying the entire parent environment and passing it to a child process can expose unrelated secrets, tokens, proxy settings, and credentials to the invoked helper script. In this skill, the child process is an external dependency discovered from the system, so inheriting all environment variables unnecessarily broadens the blast radius if that dependency is compromised or replaced.
prompt = f"{character_desc} The robot looks friendly and slightly mischievous. Square composition."
print(f"Generating base character...")
env = os.environ.copy()
env["GEMINI_API_KEY"] = api_key
result = subprocess.run(
This line repeats the same pattern of forwarding the full process environment to an external helper, which can leak unrelated environment secrets to child code. Because the skill's purpose is image generation for a holocube, broad environment inheritance is not necessary for functionality and therefore increases risk without clear benefit.
out_path = output_dir / f"{emote}.png"
prompt = f"Make this robot head {desc} Keep the same wireframe holographic style on pure black background. Same character."
env = os.environ.copy()
env["GEMINI_API_KEY"] = api_key
result = subprocess.run(
The skill instructs the agent to use shell, network discovery, file writes, and environment-dependent sprite generation, but it declares no explicit tool scope or permissions. That makes the operational boundary unclear and can lead to an agent invoking powerful capabilities such as subnet scanning, device modification, and file deletion without prior restriction or user visibility.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
return p
# Try finding via npm
try:
result = subprocess.run(["npm", "root", "-g"], capture_output=True, text=True)
root = result.stdout.strip()
p = os.path.join(root, "openclaw/skills/nano-banana-pro/scripts/generate_image.py")
if os.path.exists(p):
The script reads a Gemini API key from the user's global OpenClaw configuration even though its visible task is local sprite generation. Accessing secrets from a broader application config increases secret exposure and expands trust to any downstream component invoked by this script, especially since the key is then forwarded to another script through the environment.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
env = os.environ.copy()
env["GEMINI_API_KEY"] = api_key
result = subprocess.run(
["uv", "run", script_path, "--prompt", prompt, "--filename", str(base_path), "--resolution", "1K"],
capture_output=True, text=True, timeout=60, env=env,
)
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
env = os.environ.copy()
env["GEMINI_API_KEY"] = api_key
result = subprocess.run(
["uv", "run", script_path, "--prompt", prompt, "--filename", str(out_path), "-i", str(base_path), "--resolution", "1K"],
capture_output=True, text=True, timeout=60, env=env,
)
The script accepts a user-supplied --ip value and uses it directly in an HTTP request target, allowing the process to connect to arbitrary hosts rather than only the intended holocube device. In an agent or automation context, untrusted input could abuse this to probe internal services or reach unexpected endpoints from the machine running the skill.
"""Check holocube connectivity and storage."""
ip = ip or DEFAULT_IP
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=3) as r:
info = json.loads(r.read())
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
This second request reuses the same unvalidated --ip-derived destination, expanding the attacker-controlled network interaction to another endpoint on the target host. If an agent can be induced to run this with arbitrary input, it can be used for internal network enumeration and limited SSRF-style probing from the local environment.
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=3) as r:
info = json.loads(r.read())
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
print(f"Model: {info.get('m', 'unknown')}")
print(f"Version: {info.get('v', 'unknown')}")
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
"""Scan local subnet for GeekMagic devices."""
def check(ip):
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=1) as r:
data = json.loads(r.read())
if "m" in data:
return ip, data
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
urllib.request.urlopen(f"http://{ip}/set?img=%2Fimage%2Fadam-neutral.gif", timeout=3)
print("\n Set to Photo Album, default: neutral")
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
print(f" Storage: {space.get('free', 0) // 1024}KB free")
The onboarding script silently reads a Gemini API key from an unrelated local OpenClaw configuration path rather than limiting itself to credentials the user explicitly provides for this setup. Cross-skill credential harvesting increases the blast radius of a compromised or overly broad skill and violates least-privilege expectations, especially because the key is then reused by another component for external API calls.
Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.
def check_device(ip):
"""Verify holocube is reachable."""
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=3) as r:
info = json.loads(r.read())
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.
def check_device(ip):
"""Verify holocube is reachable."""
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=3) as r:
info = json.loads(r.read())
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.
def check_device(ip):
"""Verify holocube is reachable."""
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=3) as r:
info = json.loads(r.read())
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.
def check_device(ip):
"""Verify holocube is reachable."""
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=3) as r:
info = json.loads(r.read())
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.
def check_device(ip):
"""Verify holocube is reachable."""
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=3) as r:
info = json.loads(r.read())
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.
def check_device(ip):
"""Verify holocube is reachable."""
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=3) as r:
info = json.loads(r.read())
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.
def check_device(ip):
"""Verify holocube is reachable."""
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=3) as r:
info = json.loads(r.read())
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.
def check_device(ip):
"""Verify holocube is reachable."""
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=3) as r:
info = json.loads(r.read())
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.
def check_device(ip):
"""Verify holocube is reachable."""
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=3) as r:
info = json.loads(r.read())
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.
def check_device(ip):
"""Verify holocube is reachable."""
try:
with urllib.request.urlopen(f"http://{ip}/v.json", timeout=3) as r:
info = json.loads(r.read())
with urllib.request.urlopen(f"http://{ip}/space.json", timeout=3) as r:
space = json.loads(r.read())
No suspicious patterns detected.