T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/send_gif.py:84
- Finding
- Redirect Allowlist Bypass and Unbounded Media Response Buffering<![CDATA[ ## Vulnerability Details **File Location**: `scripts/send_gif.py`, lines 84-98 **Vulnerability Type**: Redirect validation bypass and denial of service through unbounded response buffering **Risk Level**: Medium ### Vulnerable Code ```python def download_media(url: str, out_dir: Path, min_bytes: int, max_bytes: int, allowed_hosts, retries: int = 3): if not is_allowed_host(url, allowed_hosts): raise ValueError("url host is not allowed by policy") out_dir.mkdir(parents=True, exist_ok=True) last = None for i in range(retries): try: req = urllib.request.Request(url, headers={"User-Agent": "openclaw-whatsapp-gif/1.5"}) with urllib.request.urlopen(req, timeout=20) as resp: data = resp.read() content_type = resp.headers.get("Content-Type", "") if len(data) < min_bytes: raise ValueError(f"media too small ({len(data)} bytes)") if len(data) > max_bytes: raise ValueError(f"media too large ({len(data)} bytes)") ext = infer_extension(url, content_type) if ext not in {".mp4", ".gif", ".webm"}: raise ValueError(f"unsupported content type: {content_type or 'unknown'}") ``` ### Technical Analysis The function validates only the hostname in the initial candidate URL. Python's `urllib.request.urlopen` follows HTTP redirects automatically, but the code does not validate `resp.geturl()` or otherwise confirm that the final response still belongs to an allowed host. Consequently, an approved media URL that redirects can cross the configured network trust boundary. This is relevant if an approved provider is compromised, returns an attacker-influenced redirect, or exposes an open-redirect behavior. The configured `maxBytes` limit also does not impose an actual download or memory limit. `resp.read()` buffers the entire response in process memory before `len(data)` is compared with `max_byte ...[truncated 1764 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Validate the final response URL: - Disable automatic redirects and handle each redirect explicitly. - Validate every redirect destination against `allowedMediaHosts`. - After opening a response, verify `resp.geturl()` before reading its body. - Permit only HTTPS and reject embedded credentials, nonstandard schemes, and unexpected ports. 2. Stream downloads with a hard limit: - Read in bounded chunks rather than calling `resp.read()` without a size. - Abort immediately once the accumulated size exceeds `maxBytes`. - Reject a declared `Content-Length` greater than the configured limit, while still enforcing the streaming limit because the header may be absent or false. 3. Validate actual media content: - Require an allowlisted MIME type. - Inspect magic bytes or parse the media with a trusted decoder. - Ensure the detected file format agrees with the selected extension. - Reject HTML, JSON, text, and polyglot content. 4. Add tests covering: - Redirects from an approved host to an unapproved host. - Redirect chains. - Oversized chunked responses. - Incorrect MIME types and forged media extensions. ]]>
