T09 · Insecure Skill Coding Practices
- Location
oasis/server.py:118- Finding
Unauthenticated callback URL leaks the privileged internal service token
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
TeamClaw is a coherent multi-agent orchestration platform, but it grants broad automation and credential-bearing service access with weak scoping that should be reviewed before installation.
Install only if you are comfortable running a powerful local multi-agent service with code execution, persistent memory, stored API keys, and optional public access. Keep it bound to localhost unless you have reviewed the exposed routes, avoid exposing OASIS directly, do not use untrusted callback URLs or external expert API URLs, rotate tokens if exposure is suspected, and prefer running it in a separate low-privilege account or container.
oasis/server.py:118Unauthenticated callback URL leaks the privileged internal service token
oasis/engine.py:163Attacker-controlled external expert URL can exfiltrate the OpenClaw API key
src/mcp_commander.py:256Advertised command sandbox permits unrestricted host-level code execution
oasis/server.py:531OASIS APIs trust caller-supplied identities and permit path traversal
scripts/setup_env.sh:13Setup and startup execute mutable remote payloads without integrity verification
This endpoint accepts arbitrary POST bodies and forwards the caller's Authorization header directly to the internal agent service, while also returning permissive CORS headers on OPTIONS. In a system exposing powerful local agent capabilities, this creates a confused-deputy proxy that can let web clients drive sensitive backend actions through the frontend without strong validation or origin restrictions.
# 直接透传请求体和 Authorization header 到后端
auth_header = request.headers.get("Authorization", "")
try:
r = requests.post(
LOCAL_OPENAI_COMPLETIONS_URL,
json=request.get_json(silent=True),
headers={
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
def proxy_openai_models():
"""透传 /v1/models"""
try:
r = requests.get(f"http://127.0.0.1:{PORT_AGENT}/v1/models", timeout=10)
return Response(r.content, status=r.status_code, content_type=r.headers.get("content-type", "application/json"))
except Exception as e:
return jsonify({"error": str(e)}), 500
The login handler forwards raw credentials to a backend and then stores the password in the Flask session for later reuse. Storing reusable credentials server-side in session state increases exposure through session compromise, memory disclosure, logging mistakes, or weak cookie/session protections.
password = request.json.get("password", "")
try:
r = requests.post(LOCAL_LOGIN_URL, json={"user_id": user_id, "password": password}, timeout=10)
if r.status_code == 200:
# 登录成功,在 Flask session 中记录
session["user_id"] = user_id
This endpoint reuses credentials stored in session and forwards them to the backend to trigger cancellation actions. The core issue is the insecure credential handling pattern; compromise of the frontend session enables backend action execution as the user.
return jsonify({"error": "未登录"}), 401
session_id = request.json.get("session_id", "default") if request.is_json else "default"
try:
r = requests.post(LOCAL_AGENT_CANCEL_URL, json={"user_id": user_id, "password": password, "session_id": session_id}, timeout=5)
return jsonify(r.json())
except Exception as e:
return jsonify({"error": str(e)}), 500
The TTS proxy forwards authenticated user data and returns raw media from the backend without additional authorization controls beyond the session. Because credentials are stored and replayed, a stolen session can be abused to invoke backend functionality and potentially process sensitive text.
payload = {"user_id": user_id, "password": password, "text": text}
if voice:
payload["voice"] = voice
r = requests.post(LOCAL_TTS_URL, json=payload, timeout=60)
if r.status_code != 200:
return jsonify({"error": f"TTS 服务错误: {r.status_code}"}), r.status_code
The frontend exposes an internal tools listing endpoint and authenticates to the backend with a static internal token from environment. If this frontend is reachable by untrusted users, it can disclose privileged internal capability metadata that should not be broadly exposed.
def proxy_tools():
"""代理获取工具列表请求到后端 Agent"""
try:
r = requests.get(LOCAL_TOOLS_URL, headers={"X-Internal-Token": INTERNAL_TOKEN}, timeout=10)
return jsonify(r.json())
except Exception as e:
return jsonify({"error": str(e), "tools": []}), 500
User credentials are sent as URL query parameters to the backend on a GET request. Query-string credentials are especially risky because they may be logged by servers, proxies, monitoring systems, and browser tooling more easily than body data or headers.
if not user_id or not password:
return jsonify({"error": "未登录"}), 401
try:
r = requests.get(LOCAL_SETTINGS_URL, params={"user_id": user_id, "password": password}, timeout=10)
return jsonify(r.json()), r.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
This settings update route injects stored user credentials into outbound requests to the backend. The repeated credential replay pattern magnifies risk from any session compromise and unnecessarily broadens credential exposure surface.
data = request.get_json(force=True)
data["user_id"] = user_id
data["password"] = password
r = requests.post(LOCAL_SETTINGS_URL, json=data, timeout=10)
return jsonify(r.json()), r.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
The session-list proxy forwards stored credentials to retrieve user data from the backend. While common in a proxy design, the insecure aspect is retaining and replaying passwords for routine actions rather than using scoped tokens.
if not user_id or not password:
return jsonify({"error": "未登录"}), 401
try:
r = requests.post(LOCAL_SESSIONS_URL, json={"user_id": user_id, "password": password}, timeout=15)
return jsonify(r.json()), r.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
This route forwards stored credentials to an internal status endpoint for authenticated actions. The danger comes from session-based credential replay enabling unauthorized backend access if the frontend session is hijacked or abused via CSRF.
if not user_id or not password:
return jsonify({"error": "未登录"}), 401
try:
r = requests.post(
f"http://127.0.0.1:{PORT_AGENT}/sessions_status",
json={"user_id": user_id, "password": password},
timeout=5,
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
"""Proxy to fetch OpenClaw session list from OASIS server."""
filter_kw = request.args.get("filter", "")
try:
r = requests.get(
f"{OASIS_BASE_URL}/sessions/openclaw",
params={"filter": filter_kw},
timeout=10,
The session-history endpoint replays stored credentials to fetch message history from the backend. If the frontend session is compromised, an attacker can access potentially sensitive conversation history through this proxy.
return jsonify({"error": "未登录"}), 401
sid = request.json.get("session_id", "")
try:
r = requests.post(LOCAL_SESSION_HISTORY_URL, json={
"user_id": user_id, "password": password, "session_id": sid
}, timeout=15)
return jsonify(r.json()), r.status_code
This route forwards stored credentials to check session status, continuing the same insecure authentication pattern. Although lower impact than destructive actions, it still leaks access via session compromise and supports user activity enumeration.
return jsonify({"has_new_messages": False}), 200
sid = request.json.get("session_id", "") if request.is_json else ""
try:
r = requests.post(LOCAL_SESSION_STATUS_URL, json={
"user_id": user_id, "password": password, "session_id": sid
}, timeout=5)
return jsonify(r.json()), r.status_code
The delete-session proxy enables destructive backend actions using credentials stored in the frontend session. A hijacked browser session or cross-site request against this endpoint could irreversibly delete user data or workflow history.
return jsonify({"error": "未登录"}), 401
sid = request.json.get("session_id", "") if request.is_json else ""
try:
r = requests.post(LOCAL_DELETE_SESSION_URL, json={
"user_id": user_id, "password": password, "session_id": sid
}, timeout=15)
return jsonify(r.json()), r.status_code
Group chat APIs use an Authorization header built as Bearer user_id:password, embedding plaintext credentials into a bearer token format and forwarding them to the backend. This is a weak authentication design that increases credential exposure and makes any leaked header immediately reusable.
if not uid:
return jsonify([]), 200
try:
r = requests.get(f"http://127.0.0.1:{PORT_AGENT}/groups", headers=headers, timeout=10)
return jsonify(r.json()), r.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
This group-creation route forwards arbitrary request JSON together with a header containing plaintext credentials encoded as a bearer token. The issue is not the proxy itself but the insecure credential construction and replay across many state-changing endpoints.
return jsonify({"error": "未登录"}), 401
try:
headers["Content-Type"] = "application/json"
r = requests.post(f"http://127.0.0.1:{PORT_AGENT}/groups", json=request.get_json(silent=True), headers=headers, timeout=10)
return jsonify(r.json()), r.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
Fetching group details relies on the same insecure header format carrying raw credentials to the backend. This unnecessarily expands exposure of user secrets across multiple requests and endpoints.
if not uid:
return jsonify({"error": "未登录"}), 401
try:
r = requests.get(f"http://127.0.0.1:{PORT_AGENT}/groups/{group_id}", headers=headers, timeout=10)
return jsonify(r.json()), r.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
Updating group state is a privileged action performed through a proxy that sends plaintext-derived bearer credentials to the backend. If a session or header is stolen, an attacker can modify group configuration and potentially disrupt multi-agent workflows or messaging.
return jsonify({"error": "未登录"}), 401
try:
headers["Content-Type"] = "application/json"
r = requests.put(f"http://127.0.0.1:{PORT_AGENT}/groups/{group_id}", json=request.get_json(silent=True), headers=headers, timeout=10)
return jsonify(r.json()), r.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
This messages endpoint exposes group message retrieval through a credential-replay proxy. Compromise of the frontend session enables reading potentially sensitive group communications and coordination data.
return jsonify({"messages": []}), 200
try:
after_id = request.args.get("after_id", "0")
r = requests.get(
f"http://127.0.0.1:{PORT_AGENT}/groups/{group_id}/messages",
params={"after_id": after_id},
headers=headers, timeout=10,
Posting group messages through the proxy permits authenticated state-changing actions using replayed credentials. In this orchestration product, forged messages could manipulate operators or downstream agents and alter collaborative workflows.
return jsonify({"error": "未登录"}), 401
try:
headers["Content-Type"] = "application/json"
r = requests.post(
f"http://127.0.0.1:{PORT_AGENT}/groups/{group_id}/messages",
json=request.get_json(silent=True),
headers=headers, timeout=10,
Muting a group is a privileged state change performed with replayed credentials from the frontend. Session compromise can be used to disrupt notifications and coordination among users or agents.
if not uid:
return jsonify({"error": "未登录"}), 401
try:
r = requests.post(
f"http://127.0.0.1:{PORT_AGENT}/groups/{group_id}/mute",
headers=headers, timeout=10,
)
Unmuting uses the same insecure pattern as muting, allowing backend state changes through a credential-replay proxy. This supports unauthorized manipulation of user communications if the frontend session is abused.
if not uid:
return jsonify({"error": "未登录"}), 401
try:
r = requests.post(
f"http://127.0.0.1:{PORT_AGENT}/groups/{group_id}/unmute",
headers=headers, timeout=10,
)
Mute-status retrieval is read-only but still depends on forwarding raw-credential-derived authorization to the backend. The main risk is unnecessary credential exposure and information disclosure through session compromise.
if not uid:
return jsonify({"muted": False}), 200
try:
r = requests.get(
f"http://127.0.0.1:{PORT_AGENT}/groups/{group_id}/mute_status",
headers=headers, timeout=10,
)
This route reveals which sessions can join a group via a proxy authorized by replayed credentials. In a multi-agent system, such metadata can expose topology, available agents, or active workflow structure to anyone who obtains the session.
if not uid:
return jsonify({"sessions": []}), 200
try:
r = requests.get(
f"http://127.0.0.1:{PORT_AGENT}/groups/{group_id}/sessions",
headers=headers, timeout=15,
)
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
return jsonify([]), 200
try:
print(f"[OASIS Proxy] Fetching topics from {OASIS_BASE_URL}/topics for user={user_id}")
r = requests.get(f"{OASIS_BASE_URL}/topics", params={"user_id": user_id}, timeout=10)
print(f"[OASIS Proxy] Response status: {r.status_code}, count: {len(r.json()) if r.text else 0}")
return jsonify(r.json()), r.status_code
except Exception as e:
No suspicious patterns detected.