T09 · Insecure Skill Coding Practices
- Location
scripts/juji.py:42- Finding
Sensitive Agent Credentials Stored Without Enforced File Permissions
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This skill appears to be a real JuJi community integration, but it handles persistent agent credentials and broad state-changing community actions with weak scoping and security controls.
Install only if you trust the JuJi backend you configure and are comfortable with the skill acting as your Agent for publishing, governance votes, tasks, and asset workflows. Use HTTPS for any non-local backend, restrict ~/.juji/.env to owner-only permissions, avoid storing unrelated secrets in ~/.openclaw/.env, and rotate the JuJi token/key if they may have been exposed.
scripts/juji.py:42Sensitive Agent Credentials Stored Without Enforced File Permissions
scripts/juji.py:91Authenticated Traffic and WebSocket Tokens Can Be Sent Over Cleartext Transport
requirements.txt:1Security-Sensitive Dependencies Are Not Reproducibly Pinned
scripts/juji.py:23Skill Reads a Shared OpenClaw Credential File Beyond Its Scoped Configuration
The description frames the skill as a generic registration/WebSocket shell, but the documentation shows it also performs many predefined high-impact state-changing operations such as publishing content, governance voting, committee actions, task actions, and asset issuance workflows, plus local credential persistence. This mismatch can mislead operators into granting trust and permissions under a narrower mental model than the skill actually needs, which is especially risky because it interacts with remote capabilities discovered dynamically at runtime.
Referenced artifact was not completely inspected
- 仅文档:`GET {JUJI_BASE_URL}/skills/juji/download?format=raw` → 保存为 `SKILL.md`。
Package name closely resembles a popular package, suggesting possible typosquatting. Attackers publish malicious packages with similar names to trick developers into installing them.
The skill reads secrets and identity material from ~/.juji/.env and ~/.openclaw/.env, including a private key and token, without any trust-boundary restriction between tools. Cross-reading another application's env file increases the blast radius: compromise or confusion in one tool can expose credentials usable by this skill, enabling agent impersonation or unauthorized service access.
if v:
return v
for env_path in [
pathlib.Path.home() / ".juji" / ".env",
pathlib.Path.home() / ".openclaw" / ".env",
]:
if not env_path.exists():
Reading credentials from another tool's .env file creates a shared-secret trust boundary that can leak or misuse tokens/private keys across applications. In an agent skill context, this is more dangerous because the credentials are then used automatically to establish authenticated WebSocket sessions and sign messages.
return v
for env_path in [
pathlib.Path.home() / ".juji" / ".env",
pathlib.Path.home() / ".openclaw" / ".env",
]:
if not env_path.exists():
continue
The code writes secrets to ~/.juji/.env in plaintext with default permissions and no permission hardening. Any local user or process with filesystem access to that file could recover the WebSocket token or private key and authenticate as the agent.
def save_values(values: dict):
env_path = pathlib.Path.home() / ".juji" / ".env"
env_path.parent.mkdir(parents=True, exist_ok=True)
old = env_path.read_text(encoding="utf-8", errors="ignore") if env_path.exists() else ""
kv = {}
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
token = load_value("JUJI_AGENT_TOKEN")
if not token:
raise SystemExit(
"JUJI_AGENT_TOKEN 缺失:请先执行注册(如 init / 任意带子命令)以写入 ~/.juji/.env 中的 ws_token"
)
q = {"agent_id": agent_id, "token": token}
public_key = load_value("JUJI_AGENT_PUBLIC_KEY")
The skill declares no explicit tool scope or allowed-tools restrictions while its documented behavior requires environment access, file reads/writes, and network communication, including persistent WebSocket connections and writing credentials to local .env files. In an agent framework, this creates an over-privileged installation surface where users and orchestrators cannot accurately constrain what the skill may access, increasing the blast radius if the backend is compromised or the skill is misused.
This markdown file contains user-facing operational instructions almost exclusively in Chinese, including setup, commands, and safety-relevant behavior notes. That effectively enforces a specific language/locale for users without documenting a choice, which matches the language-policy violation criteria.
The skill generates a long-lived private signing key and stores it in ~/.juji/.env without setting restrictive filesystem permissions or clearly warning the user. On multi-user systems or misconfigured environments, other local processes or users could read the key and impersonate the agent for WebSocket authentication.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
payload["wallet_address"] = wallet_address
if name:
payload["name"] = name
r = requests.post(f"{base_url}/agent/register", json=payload, timeout=30)
r.raise_for_status()
agent = r.json()
ws_token = agent.get("ws_token")
Skill allows unbounded resource consumption (API calls, storage, compute). Without rate limits or quotas, a compromised or misbehaving agent can cause denial-of-service or cost overruns.
session_active = True
while session_active and not user_done:
try:
line = stdin_q.get(timeout=0.5)
except queue.Empty:
if connection_lost.is_set():
print_out(
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
"reason": args.reason,
"extra_meta": json.loads(args.extra_meta or "{}"),
}
r = requests.post(f"{base_url}/asset/issuance/apply", json=payload, timeout=30)
r.raise_for_status()
return r.json()
print_out(with_ws(base_url, run_asset), args.format)
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
"reason": args.reason,
"extra_meta": json.loads(args.extra_meta or "{}"),
}
r = requests.post(f"{base_url}/asset/issuance/apply", json=payload, timeout=30)
r.raise_for_status()
return r.json()
print_out(with_ws(base_url, run_asset), args.format)
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
"author_agent_id": args.author_agent_id or agent["agent_id"],
"tags": args.tags or [],
}
r = requests.post(f"{base_url}/content/publish", json=payload, timeout=30)
r.raise_for_status()
return r.json()
print_out(with_ws(base_url, run_pub), args.format)
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
"author_agent_id": args.author_agent_id or agent["agent_id"],
"tags": args.tags or [],
}
r = requests.post(f"{base_url}/content/publish", json=payload, timeout=30)
r.raise_for_status()
return r.json()
print_out(with_ws(base_url, run_pub), args.format)
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
"reward_wallet_address": args.reward_wallet_address,
"deadline": args.deadline,
}
r = requests.post(f"{base_url}/task/create", json=payload, timeout=30)
r.raise_for_status()
return r.json()
print_out(with_ws(base_url, run_task_create), args.format)
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
"reward_wallet_address": args.reward_wallet_address,
"deadline": args.deadline,
}
r = requests.post(f"{base_url}/task/create", json=payload, timeout=30)
r.raise_for_status()
return r.json()
print_out(with_ws(base_url, run_task_create), args.format)
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
if args.cmd == "task-join":
def run_task_join(_ws, agent):
payload = {"task_id": args.task_id, "agent_id": args.agent_id or agent["agent_id"]}
r = requests.post(f"{base_url}/task/join", params=payload, timeout=30)
r.raise_for_status()
return r.json()
print_out(with_ws(base_url, run_task_join), args.format)
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
"result": args.result,
"proof": args.proof,
}
r = requests.post(f"{base_url}/task/submit", json=payload, timeout=30)
r.raise_for_status()
return r.json()
print_out(with_ws(base_url, run_task_submit), args.format)
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.
"result": args.result,
"proof": args.proof,
}
r = requests.post(f"{base_url}/task/submit", json=payload, timeout=30)
r.raise_for_status()
return r.json()
print_out(with_ws(base_url, run_task_submit), args.format)
The dependency is specified with a lower bound only (requests>=2.28.0), which makes builds non-reproducible and allows future installs to pull different versions over time. In a network-facing skill that relies on HTTP communication, this increases supply-chain risk and makes it harder to ensure that a known-safe version is installed.
requests>=2.28.0
websocket-client>=1.8.0
pynacl>=1.5.0
requests has multiple known advisories, and because the manifest does not pin an exact version, there is no way to verify from this file whether the installed version is affected or fixed. In a skill that likely performs backend HTTP calls, this uncertainty meaningfully raises risk even though the file alone does not prove exploitation.
The dependency websocket-client>=1.8.0 is unpinned, so installations may resolve to different versions depending on when and where the skill is deployed. Because this skill maintains a persistent WebSocket connection, uncontrolled dependency drift can expose it to newly introduced or unreviewed package behavior.
requests>=2.28.0
websocket-client>=1.8.0
pynacl>=1.5.0
Using pynacl>=1.5.0 without an upper bound or exact pin permits arbitrary newer releases to be installed, which weakens build reproducibility and supply-chain assurance. Since this package is commonly used for cryptographic operations, version ambiguity can be especially risky if an affected release is later pulled in.
requests>=2.28.0
websocket-client>=1.8.0
pynacl>=1.5.0
No suspicious patterns detected.