T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/choir.py:327
- Finding
- Unvalidated Custom Provider Endpoint Can Receive API Credentials and User Data<![CDATA[ ## Vulnerability Details **File Location**: `scripts/choir.py:327-332`, with credential transmission at `scripts/choir.py:916-937` and `scripts/choir.py:952-969` **Vulnerability Type**: Arbitrary credential forwarding, insecure endpoint configuration, and server-side request forgery **Risk Level**: Medium ### Complete Code Snippet ```python # scripts/choir.py:327-332 base = cfg.get("base") or DEFAULT_BASE.get(name, "") kind = cfg.get("kind") or ("gemini" if name in ("gemini", "google") else "openai") model = cfg.get("model") or cfg.get("default_model") or DEFAULT_MODEL.get(name, "") tags = set(cfg.get("tags", []) or []) self.providers[name] = { "keys": keys, "base": base, "kind": kind, "model": model, "tags": tags, ``` ```python # scripts/choir.py:916-937 def _call_openai(m, prompt, system, max_tokens, timeout, image_path): base = m["base"] or DEFAULT_BASE.get(m["provider"], "") url = base.rstrip("/") + "/chat/completions" headers = {"Authorization": "Bearer " + m["key"], "Content-Type": "application/json", "User-Agent": f"sith-choir/{VERSION}"} messages = [] if system: messages.append({"role": "system", "content": system}) if image_path: mime, b64 = _image_data_uri(image_path) messages.append({"role": "user", "content": [ {"type": "text", "text": prompt}, {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}]}) else: messages.append({"role": "user", "content": prompt}) body = {"model": m["model"], "messages": messages, "max_tokens": max_tokens, "temperature": 0.4} for attempt in (1, 2): try: t0 = time.monotonic() data = _http_json(url, headers, body, timeout) ``` ```python # scripts/choir.py:952-969 def _call_gemini(m, prompt, system, max_tokens, timeout, image_path): base = m["base"] or DEFAULT_BASE.get(m["provider"], "") mdl = m["model ...[truncated 3778 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Parse custom endpoints with `urllib.parse.urlsplit()` and reject malformed URLs. 2. Require `https` for every credential-bearing request. Permit plaintext HTTP only through an explicit development-only option that refuses real credentials. 3. Enforce an allowlist of approved provider hostnames corresponding to the declared outbound network policy. 4. If custom OpenAI-compatible endpoints are required, place them behind an explicit flag such as `--allow-custom-base` and display the exact destination before sending credentials. 5. Resolve destination hostnames and reject loopback, link-local, multicast, reserved, and private IP ranges unless a separate, clearly documented local-endpoint option is enabled. 6. Reject URLs containing embedded user information, fragments, unexpected ports, or ambiguous hostname encodings. 7. Disable redirects for credential-bearing requests or independently validate every redirect target. Never forward authorization headers across origins. 8. Bind credentials to expected origins in the credentials schema, rather than allowing any key to be combined with any arbitrary `base`. 9. Add offline tests covering HTTP rejection, malicious hosts, internal IP addresses, encoded IP representations, DNS resolution, and cross-origin redirects. 10. Update the documentation to distinguish approved provider endpoints from explicitly trusted custom endpoints and explain that custom endpoints receive keys and all submitted content. ]]>
