fix: CivitAI downloads never follow redirects

This commit is contained in:
Vito Sansevero
2026-10-08 18:01:34 -07:00
parent b836e7cef9
commit 2d3fc1cd08
2 changed files with 92 additions and 4 deletions
+22 -1
View File
@@ -307,6 +307,27 @@ MAX_CIVITAI_DOWNLOAD_BYTES = 50 * 1024 * 1024
_DOWNLOAD_CHUNK_BYTES = 64 * 1024
class _RefuseRedirects(urllib.request.HTTPRedirectHandler):
"""Never follow a redirect: urllib would re-send the Bearer header to it."""
def redirect_request(self, req, fp, code, msg, headers, newurl):
logger.warning(f"Refusing redirect from {req.full_url} to {newurl}")
return None
def _build_opener(*handlers) -> urllib.request.OpenerDirector:
"""Build the outbound opener; extra handlers let tests serve canned responses."""
return urllib.request.build_opener(_RefuseRedirects, *handlers)
_opener = _build_opener()
def _open_url(req: urllib.request.Request, timeout: float = 10):
"""Open ``req`` through the redirect-refusing opener."""
return _opener.open(req, timeout=timeout)
def is_civitai_url(url: str) -> bool:
"""True when ``url`` is an HTTPS URL on an allow-listed CivitAI host."""
try:
@@ -355,7 +376,7 @@ def _download_one(url: str, local_path: Path, api_key: str) -> Optional[str]:
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=10) as resp:
with _open_url(req, timeout=10) as resp:
raw = _read_capped(resp, MAX_CIVITAI_DOWNLOAD_BYTES)
if raw is None:
logger.warning(f"Refusing oversized download: {url}")
+70 -3
View File
@@ -5,6 +5,7 @@ Tests metadata parsing, trigger word extraction, image URL extraction,
directory detection, TriggerWordCache, and image download logic.
"""
import email
import json
import os
import shutil
@@ -12,6 +13,10 @@ import sys
import tempfile
import threading
import unittest
import urllib.parse
import urllib.request
import urllib.response
from io import BytesIO
from pathlib import Path
from unittest.mock import patch
@@ -323,7 +328,7 @@ class TestDownloadOne(unittest.TestCase):
self.target = self.tmpdir / "out.jpg"
def _run(self, url, opener, api_key="secret-key"):
with patch("urllib.request.urlopen", opener):
with patch.object(lora_utils, "_open_url", opener):
return lora_utils._download_one(url, self.target, api_key)
def test_off_host_url_is_refused_without_a_request(self):
@@ -384,6 +389,68 @@ class TestDownloadOne(unittest.TestCase):
self.assertFalse(self.target.exists())
class _FakeHTTPSHandler(urllib.request.HTTPSHandler):
"""Serves canned responses through a real urllib opener, recording requests.
CivitAI hosts answer with a 302 to ``redirect_to``; every other host
answers 200 with a PNG body.
"""
def __init__(self, redirect_to):
super().__init__()
self.redirect_to = redirect_to
self.requests = []
def https_open(self, req):
self.requests.append(req)
host = (urllib.parse.urlsplit(req.full_url).hostname or "").lower()
if host in CIVITAI_HOSTS:
headers = email.message_from_string(f"Location: {self.redirect_to}\n")
return urllib.response.addinfourl(BytesIO(b""), headers, req.full_url, 302)
headers = email.message_from_string("Content-Type: image/png\n")
return urllib.response.addinfourl(
BytesIO(_png_bytes()), headers, req.full_url, 200
)
class TestRedirectsAreRefused(unittest.TestCase):
def setUp(self):
self.tmpdir = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, self.tmpdir, True)
def test_redirect_to_other_host_is_not_followed(self):
handler = _FakeHTTPSHandler("https://evil.example/steal")
opener = lora_utils._build_opener(handler)
with patch.object(lora_utils, "_opener", opener):
result = lora_utils._download_one(
"https://civitai.com/api/download/1", self.tmpdir / "x.jpg", "key"
)
self.assertIsNone(result)
self.assertEqual(
[r.full_url for r in handler.requests],
["https://civitai.com/api/download/1"],
)
for req in handler.requests:
host = urllib.parse.urlsplit(req.full_url).hostname
if host not in CIVITAI_HOSTS:
self.assertIsNone(req.get_header("Authorization"))
def test_redirect_to_civitai_is_not_followed_either(self):
handler = _FakeHTTPSHandler("https://image.civitai.com/other.png")
opener = lora_utils._build_opener(handler)
with patch.object(lora_utils, "_opener", opener):
result = lora_utils._download_one(
"https://civitai.com/api/download/1", self.tmpdir / "x.jpg", "key"
)
self.assertIsNone(result)
self.assertEqual(len(handler.requests), 1)
class TestDownloadCivitaiImages(unittest.TestCase):
def setUp(self):
@@ -402,7 +469,7 @@ class TestDownloadCivitaiImages(unittest.TestCase):
]
)
with patch("urllib.request.urlopen", opener):
with patch.object(lora_utils, "_open_url", opener):
paths = download_civitai_images(
metadata, self.meta_path, self.tmpdir / "cache", "key"
)
@@ -418,7 +485,7 @@ class TestDownloadCivitaiImages(unittest.TestCase):
metadata = _make_metadata(images=[{"url": "https://civitai.com/a.png"}])
cache = self.tmpdir / "cache"
with patch("urllib.request.urlopen", opener):
with patch.object(lora_utils, "_open_url", opener):
first = download_civitai_images(metadata, self.meta_path, cache, "")
second = download_civitai_images(metadata, self.meta_path, cache, "")