- Location
- scripts/_common.py:132
- Finding
- Unrestricted provider-supplied media downloads permit blind SSRF and resource exhaustion<![CDATA[
## Vulnerability Details
**File Locations**:
- `scripts/_common.py:132-146`
- `scripts/agnes_image.py:70-79`
- `scripts/agnes_video.py:204-214`
- `scripts/kolors_image.py:64-73`
- `scripts/sensenova_image.py:66-75`
**Vulnerability Type**: Server-side request forgery and unbounded resource consumption
**Risk Level**: Medium
### Vulnerable Code
The shared download function accepts an arbitrary URL, follows redirects through `urllib`, and reads the complete response into memory without a size limit:
```python
def download(url, path, retries=3, timeout=120):
"""Download a binary URL to the specified path."""
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
last_err = None
for attempt in range(retries):
try:
req = urllib.request.Request(
url,
headers={"User-Agent": "free-media-gen/1.0"},
)
with urllib.request.urlopen(req, timeout=timeout) as resp, open(path, "wb") as out:
out.write(resp.read())
return True
except Exception as e:
last_err = e
time.sleep(2 ** attempt)
sys.stderr.write("Download failed: %s -> %s\n" % (url, last_err))
return False
```
Provider-returned URLs are passed directly into this function:
```python
video_url = None
if vid:
q = urllib.parse.urlencode({
"video_id": vid,
"model_name": args.model,
})
st4, b4 = get(root + "/agnesapi?" + q, headers)
try:
j4 = json.loads(b4)
video_url = (j4.get("metadata") or {}).get("url") or dig_url(j4)
except Exception:
pass
if not video_url:
print(json.dumps({
"ok": False,
"stage": "completed_no_url",
"task_id": tid,
"video_id": vid,
"note": "The task completed but no media URL was returned",
}))
sys.exit(1)
out_path = os.path.join(
out_dir,
"agnes_video_%s.mp4" % str(tid),
)
ok = C.download(video_url, out_path, ret
...[truncated 3449 chars]
- Remediation
- <![CDATA[
## Remediation Suggestions
1. Permit only HTTPS media URLs.
2. Maintain an explicit allowlist of approved media and CDN hostnames for each provider.
3. Validate every redirect destination rather than only the initial URL.
4. Resolve destination hostnames and reject all loopback, private, link-local, multicast, unspecified, and reserved IP addresses for both IPv4 and IPv6.
5. Re-resolve and revalidate at connection time to reduce DNS-rebinding exposure.
6. Stream responses in bounded chunks instead of calling `resp.read()` without a limit:
```python
MAX_MEDIA_BYTES = 100 * 1024 * 1024
CHUNK_SIZE = 64 * 1024
written = 0
with opener.open(req, timeout=timeout) as resp, open(path, "wb") as out:
while True:
chunk = resp.read(CHUNK_SIZE)
if not chunk:
break
written += len(chunk)
if written > MAX_MEDIA_BYTES:
raise ValueError("Media response exceeds the configured size limit")
out.write(chunk)
```
7. Check `Content-Length` when present, while still enforcing a streamed byte limit because the header may be absent or false.
8. Require expected media content types, such as approved `image/*` formats or `video/mp4`.
9. Validate file signatures after download and delete partial or invalid files.
10. Apply strict maximum lengths before decoding base64 data. Estimate decoded size from the encoded length before calling `base64.b64decode()`.
11. Use temporary files followed by an atomic rename only after validation succeeds.
12. Apply restrictive network egress controls at the runtime or container level so the Skill cannot reach cloud metadata endpoints, localhost services, or private networks.
]]>