T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/openwebui-cli.py:35
- Finding
- Bearer Token and Sensitive User Data Can Be Transmitted over Plaintext HTTP<![CDATA[ ## Vulnerability Details **File Location**: `scripts/openwebui-cli.py:35-65`, `scripts/openwebui-cli.py:124-136` **Vulnerability Type**: Plaintext transmission of credentials and sensitive data **Risk Level**: High ### Vulnerable Code ```python def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None): self.base_url = (base_url or os.getenv("OPENWEBUI_URL", "http://localhost:3000")).rstrip("/") self.token = token or os.getenv("OPENWEBUI_TOKEN") if not self.token: raise ValueError("API token required. Set OPENWEBUI_TOKEN or use --token") self.session = requests.Session() self.session.headers.update({ "Authorization": f"Bearer {self.token}", "Content-Type": "application/json" }) # Allow insecure transport for local development (localhost) if self.base_url.startswith("http://localhost") or self.base_url.startswith("http://127.0.0.1"): import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def _url(self, endpoint: str) -> str: """Build full URL from endpoint.""" return urljoin(self.base_url + "/", endpoint.lstrip("/")) def _request(self, method: str, endpoint: str, **kwargs) -> dict: """Execute HTTP request with error handling.""" url = self._url(endpoint) verify = not (self.base_url.startswith("http://localhost") or self.base_url.startswith("http://127.0.0.1")) try: response = self.session.request(method, url, verify=verify, **kwargs) response.raise_for_status() return response.json() if response.content else {} ``` The file-upload path independently sends the same bearer credential and selected file contents: ```python def upload_file(self, file_path: str, process: bool = True) -> dict: """POST /api/v1/files/ - Upload file for RAG.""" path = Path(file_path) if not path.exists(): raise FileNotFoundError(f"File not found: {file_path}") ...[truncated 3542 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Parse the configured URL with `urllib.parse.urlparse` before creating the session or sending any request. 2. Require `https` for all non-loopback destinations. 3. Permit plaintext HTTP only when the normalized hostname is exactly `localhost`, `127.0.0.1`, or `::1`. 4. Reject URLs containing unexpected credentials, unsupported schemes, or missing hostnames. 5. Replace string-prefix trust checks with parsed hostname and scheme comparisons. 6. If remote plaintext HTTP must be supported for a constrained development environment, require an explicit option such as `--allow-insecure-http`, display a prominent warning, and keep it disabled by default. 7. Centralize all network requests, including multipart uploads, through the configured `requests.Session` so transport policy is applied consistently. 8. Set explicit connection and read timeouts and consider disabling redirects or revalidating the scheme and host after every redirect to prevent credentials from being forwarded to an unintended destination. 9. Avoid accepting API tokens through command-line arguments where practical because command-line values may be visible in process listings or shell history; prefer environment variables, protected configuration files, or standard input. 10. Add tests verifying that remote `http://` URLs and deceptive hostnames such as `localhost.example.com` are rejected. A secure validation pattern would resemble: ```python import ipaddress from urllib.parse import urlparse parsed = urlparse(self.base_url) if parsed.scheme not in ("http", "https") or not parsed.hostname: raise ValueError("OPENWEBUI_URL must be a valid HTTP(S) URL") hostname = parsed.hostname.lower() is_loopback = hostname == "localhost" if not is_loopback: try: is_loopback = ipaddress.ip_address(hostname).is_loopback except ValueError: pass if parsed.scheme != "https" and not is_loopback: raise ValueError("HTTPS is required for non-loopback O ...[truncated 31 chars]
