mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-04-10 04:42:14 -03:00
fix(csp): support CivitAI CDN subdomains for example images (#822)
- Update CSP whitelist to use wildcard *.civitai.com for all CDN subdomains - Fix hostname parsing to use parsed.hostname instead of parsed.netloc (handles ports) - Update rewrite_preview_url() to support all CivitAI CDN subdomains - Update rewriteCivitaiUrl() frontend function to support subdomains - Add comprehensive tests for edge cases (ports, subdomains, invalid URLs) - Add security note explaining wildcard CSP design decision Fixes CSP blocking of images from image-b2.civitai.com and other CDN subdomains
This commit is contained in:
@@ -94,6 +94,37 @@ describe('civitaiUtils', () => {
|
||||
expect(wasRewritten).toBe(false);
|
||||
expect(rewritten).toBe('not-a-valid-url');
|
||||
});
|
||||
|
||||
it('should rewrite URLs from CivitAI CDN subdomains', () => {
|
||||
const originalUrl = 'https://image-b2.civitai.com/file/civitai-media-cache/original=true/sample.png';
|
||||
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL);
|
||||
|
||||
expect(wasRewritten).toBe(true);
|
||||
expect(rewritten).toBe('https://image-b2.civitai.com/file/civitai-media-cache/width=450,optimized=true/sample.png');
|
||||
});
|
||||
|
||||
it('should handle URLs with explicit port numbers', () => {
|
||||
const originalUrl = 'https://image.civitai.com:443/checkpoints/original=true/test.png';
|
||||
const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL);
|
||||
|
||||
expect(wasRewritten).toBe(true);
|
||||
// JavaScript URL.toString() removes default HTTPS port (443)
|
||||
expect(rewritten).toBe('https://image.civitai.com/checkpoints/width=450,optimized=true/test.png');
|
||||
});
|
||||
|
||||
it('should handle case-insensitive hostnames', () => {
|
||||
const testCases = [
|
||||
'https://IMAGE.CIVITAI.COM/original=true/test.png',
|
||||
'https://Image.Civitai.Com/original=true/test.png',
|
||||
'https://image-b2.CIVITAI.com/original=true/test.png',
|
||||
];
|
||||
|
||||
for (const url of testCases) {
|
||||
const [rewritten, wasRewritten] = rewriteCivitaiUrl(url, 'image', OptimizationMode.THUMBNAIL);
|
||||
expect(wasRewritten).toBe(true);
|
||||
expect(rewritten).toContain('width=450,optimized=true');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOptimizedUrl', () => {
|
||||
@@ -157,6 +188,23 @@ describe('civitaiUtils', () => {
|
||||
expect(isCivitaiUrl('https://image.civitai.com/')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for CivitAI CDN subdomains', () => {
|
||||
expect(isCivitaiUrl('https://image-b2.civitai.com/file/test.png')).toBe(true);
|
||||
expect(isCivitaiUrl('https://image-b3.civitai.com/test.jpg')).toBe(true);
|
||||
expect(isCivitaiUrl('https://cdn.civitai.com/test.png')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for CivitAI URLs with explicit ports', () => {
|
||||
expect(isCivitaiUrl('https://image.civitai.com:443/test.png')).toBe(true);
|
||||
expect(isCivitaiUrl('https://image-b2.civitai.com:443/file/test.jpg')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle case-insensitive hostnames', () => {
|
||||
expect(isCivitaiUrl('https://IMAGE.CIVITAI.COM/test.png')).toBe(true);
|
||||
expect(isCivitaiUrl('https://Image.Civitai.Com/test.png')).toBe(true);
|
||||
expect(isCivitaiUrl('https://image-b2.CIVITAI.com/test.png')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-CivitAI URLs', () => {
|
||||
expect(isCivitaiUrl('https://example.com/image.jpg')).toBe(false);
|
||||
expect(isCivitaiUrl('https://civitai.com/image.jpg')).toBe(false);
|
||||
|
||||
@@ -2,7 +2,10 @@ import pytest
|
||||
from aiohttp import web
|
||||
from aiohttp.test_utils import make_mocked_request
|
||||
|
||||
from py.middleware.csp_middleware import REMOTE_MEDIA_SOURCES, relax_csp_for_remote_media
|
||||
from py.middleware.csp_middleware import (
|
||||
REMOTE_MEDIA_SOURCES,
|
||||
relax_csp_for_remote_media,
|
||||
)
|
||||
|
||||
DEFAULT_CSP = (
|
||||
"default-src 'self'; "
|
||||
@@ -40,7 +43,9 @@ async def _invoke_middleware(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_relax_csp_appends_remote_sources_and_preserves_existing_directives() -> None:
|
||||
async def test_relax_csp_appends_remote_sources_and_preserves_existing_directives() -> (
|
||||
None
|
||||
):
|
||||
response = await _invoke_middleware("/some-path", web.Response())
|
||||
header_value = response.headers.get("Content-Security-Policy")
|
||||
assert header_value is not None
|
||||
@@ -48,16 +53,17 @@ async def test_relax_csp_appends_remote_sources_and_preserves_existing_directive
|
||||
directives = _parse_directives(header_value)
|
||||
|
||||
# Existing directives remain intact
|
||||
assert directives["script-src"] == ["'self'", "'unsafe-inline'", "'unsafe-eval'", "blob:"]
|
||||
assert directives["script-src"] == [
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
"'unsafe-eval'",
|
||||
"blob:",
|
||||
]
|
||||
assert directives["img-src"][:3] == ["'self'", "data:", "blob:"]
|
||||
|
||||
# Remote media hosts are added once to the relevant directives
|
||||
for source in REMOTE_MEDIA_SOURCES:
|
||||
assert source in directives["img-src"]
|
||||
|
||||
assert "media-src" in directives
|
||||
assert directives["media-src"][0] == "'self'"
|
||||
for source in REMOTE_MEDIA_SOURCES:
|
||||
assert source in directives["media-src"]
|
||||
|
||||
|
||||
|
||||
144
tests/utils/test_civitai_utils_rewrite.py
Normal file
144
tests/utils/test_civitai_utils_rewrite.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Tests for CivitAI URL utilities."""
|
||||
|
||||
import pytest
|
||||
|
||||
from py.utils.civitai_utils import rewrite_preview_url
|
||||
|
||||
|
||||
class TestRewritePreviewUrl:
|
||||
"""Test cases for rewrite_preview_url function."""
|
||||
|
||||
def test_handles_none_input(self):
|
||||
"""Should return (None, False) for None input."""
|
||||
result, was_rewritten = rewrite_preview_url(None)
|
||||
assert result is None
|
||||
assert was_rewritten is False
|
||||
|
||||
def test_handles_empty_string(self):
|
||||
"""Should return (empty_string, False) for empty input."""
|
||||
result, was_rewritten = rewrite_preview_url("")
|
||||
assert result == ""
|
||||
assert was_rewritten is False
|
||||
|
||||
def test_handles_invalid_url(self):
|
||||
"""Should return original URL and False for invalid URLs."""
|
||||
invalid_url = "not-a-valid-url"
|
||||
result, was_rewritten = rewrite_preview_url(invalid_url)
|
||||
assert result == invalid_url
|
||||
assert was_rewritten is False
|
||||
|
||||
def test_handles_url_without_scheme(self):
|
||||
"""Should return original URL and False for URLs without scheme."""
|
||||
url = "image.civitai.com/something"
|
||||
result, was_rewritten = rewrite_preview_url(url)
|
||||
assert result == url
|
||||
assert was_rewritten is False
|
||||
|
||||
def test_returns_false_for_non_civitai_domains(self):
|
||||
"""Should not rewrite URLs from other domains."""
|
||||
url = "https://example.com/image.jpg"
|
||||
result, was_rewritten = rewrite_preview_url(url)
|
||||
assert result == url
|
||||
assert was_rewritten is False
|
||||
|
||||
def test_returns_false_for_main_civitai_domain(self):
|
||||
"""Should not rewrite URLs from main civitai.com domain."""
|
||||
url = "https://civitai.com/images/123"
|
||||
result, was_rewritten = rewrite_preview_url(url)
|
||||
assert result == url
|
||||
assert was_rewritten is False
|
||||
|
||||
def test_rewrites_image_civitai_com_urls(self):
|
||||
"""Should rewrite URLs from image.civitai.com."""
|
||||
url = "https://image.civitai.com/checkpoints/original=true"
|
||||
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||
assert (
|
||||
result == "https://image.civitai.com/checkpoints/width=450,optimized=true"
|
||||
)
|
||||
assert was_rewritten is True
|
||||
|
||||
def test_rewrites_subdomain_civitai_urls(self):
|
||||
"""Should rewrite URLs from CivitAI CDN subdomains like image-b2.civitai.com."""
|
||||
url = "https://image-b2.civitai.com/file/civitai-media-cache/original=true/sample.png"
|
||||
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||
assert (
|
||||
result
|
||||
== "https://image-b2.civitai.com/file/civitai-media-cache/width=450,optimized=true/sample.png"
|
||||
)
|
||||
assert was_rewritten is True
|
||||
|
||||
def test_rewrites_multiple_subdomains(self):
|
||||
"""Should rewrite URLs from various CivitAI subdomains."""
|
||||
test_cases = [
|
||||
"https://image-b3.civitai.com/original=true/test.jpg",
|
||||
"https://cdn.civitai.com/original=true/test.png",
|
||||
]
|
||||
for url in test_cases:
|
||||
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||
assert was_rewritten is True
|
||||
assert "width=450,optimized=true" in result
|
||||
|
||||
def test_handles_urls_with_explicit_port(self):
|
||||
"""Should correctly handle URLs with explicit port numbers."""
|
||||
url = "https://image.civitai.com:443/checkpoints/original=true"
|
||||
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||
assert was_rewritten is True
|
||||
assert "width=450,optimized=true" in result
|
||||
# Port is preserved in the URL (this is acceptable behavior)
|
||||
assert ":443" in result
|
||||
|
||||
def test_rewrites_video_urls_with_transcode(self):
|
||||
"""Should rewrite video URLs with transcode parameter."""
|
||||
url = "https://image.civitai.com/videos/original=true/sample.mp4"
|
||||
result, was_rewritten = rewrite_preview_url(url, "video")
|
||||
assert (
|
||||
result
|
||||
== "https://image.civitai.com/videos/transcode=true,width=450,optimized=true/sample.mp4"
|
||||
)
|
||||
assert was_rewritten is True
|
||||
|
||||
def test_video_rewrite_uses_case_insensitive_type(self):
|
||||
"""Should handle video type case-insensitively."""
|
||||
url = "https://image.civitai.com/original=true/test.mp4"
|
||||
result1, was1 = rewrite_preview_url(url, "VIDEO")
|
||||
result2, was2 = rewrite_preview_url(url, "Video")
|
||||
assert was1 is True
|
||||
assert was2 is True
|
||||
assert "transcode=true" in result1
|
||||
assert "transcode=true" in result2
|
||||
|
||||
def test_returns_original_when_no_original_true_in_path(self):
|
||||
"""Should not rewrite URLs that don't contain /original=true."""
|
||||
url = "https://image.civitai.com/checkpoints/optimized=true"
|
||||
result, was_rewritten = rewrite_preview_url(url)
|
||||
assert result == url
|
||||
assert was_rewritten is False
|
||||
|
||||
def test_preserves_path_structure_after_rewrite(self):
|
||||
"""Should maintain path structure after rewriting."""
|
||||
url = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.png"
|
||||
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||
assert was_rewritten is True
|
||||
assert result.startswith(
|
||||
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/"
|
||||
)
|
||||
assert result.endswith("/12345.png")
|
||||
|
||||
def test_defaults_to_image_mode_when_media_type_is_none(self):
|
||||
"""Should use image optimization when media_type is None."""
|
||||
url = "https://image.civitai.com/original=true/test.png"
|
||||
result, was_rewritten = rewrite_preview_url(url, None)
|
||||
assert was_rewritten is True
|
||||
assert "transcode=true" not in result
|
||||
assert "width=450,optimized=true" in result
|
||||
|
||||
def test_case_insensitive_hostname_matching(self):
|
||||
"""Should handle case-insensitive hostname matching."""
|
||||
test_cases = [
|
||||
"https://IMAGE.CIVITAI.COM/original=true/test.png",
|
||||
"https://Image.Civitai.Com/original=true/test.png",
|
||||
"https://image-b2.CIVITAI.com/original=true/test.png",
|
||||
]
|
||||
for url in test_cases:
|
||||
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||
assert was_rewritten is True, f"Failed for URL: {url}"
|
||||
Reference in New Issue
Block a user