mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
* feat(auth): proxy OIDC provider icons server-side (#1333) Strict img-src CSP blocked external OIDC icon hosts on the login page. Loosening CSP was rejected via the MakerWorld precedent, so icons are proxied: admin sets icon_url, backend fetches and caches the bytes in a deferred BLOB column, the SPA renders from a same-origin /api/v1/auth/oidc/providers/{id}/icon endpoint.
126 lines
4.6 KiB
Python
126 lines
4.6 KiB
Python
"""Unit tests for assert_safe_public_https_url (#1333).
|
|
|
|
Stricter than the Spoolman SSRF guard — explicitly verifies that Spoolman-
|
|
allowed cases (loopback, RFC-1918, hostname "localhost") are REJECTED here.
|
|
Run alongside test_ssrf_guard.py to confirm both guards keep their distinct
|
|
semantics.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from backend.app.api.routes._oidc_helpers import assert_safe_public_https_url
|
|
|
|
# ─── Accepts ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_accepts_public_https():
|
|
assert_safe_public_https_url("https://accounts.google.com/icon.png")
|
|
|
|
|
|
def test_accepts_hostname_no_dns_resolution():
|
|
# By design — DNS resolution is intentionally not performed (consistent
|
|
# with _validate_issuer_url policy). Hostnames are out of scope here.
|
|
assert_safe_public_https_url("https://idp.internal.corp/icon.png")
|
|
|
|
|
|
# ─── Rejects: non-HTTPS schemes ─────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"url",
|
|
[
|
|
"http://example.com/icon.png",
|
|
"ftp://example.com/icon.png",
|
|
"file:///etc/passwd",
|
|
"gopher://example.com",
|
|
],
|
|
)
|
|
def test_rejects_non_https(url):
|
|
with pytest.raises(ValueError, match="https"):
|
|
assert_safe_public_https_url(url)
|
|
|
|
|
|
# ─── Rejects: numeric-encoded IP addresses ──────────────────────────────────
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"url",
|
|
[
|
|
"https://2130706433/icon.png", # decimal-encoded 127.0.0.1
|
|
"https://0x7f000001/icon.png", # hex-encoded 127.0.0.1
|
|
],
|
|
)
|
|
def test_rejects_numeric_encoded_ip(url):
|
|
with pytest.raises(ValueError, match="numeric-encoded"):
|
|
assert_safe_public_https_url(url)
|
|
|
|
|
|
# ─── Rejects: cases that Spoolman INTENTIONALLY allows ──────────────────────
|
|
# These tests are deliberately structured to mirror test_ssrf_guard.py — every
|
|
# URL here is a green case for the Spoolman guard and must be a red case here.
|
|
|
|
|
|
def test_rejects_loopback_127():
|
|
with pytest.raises(ValueError, match="loopback"):
|
|
assert_safe_public_https_url("https://127.0.0.1/icon.png")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"url",
|
|
[
|
|
"https://192.168.1.50/icon.png",
|
|
"https://10.0.0.5/icon.png",
|
|
"https://172.16.0.1/icon.png",
|
|
],
|
|
)
|
|
def test_rejects_rfc1918_private(url):
|
|
with pytest.raises(ValueError, match="private"):
|
|
assert_safe_public_https_url(url)
|
|
|
|
|
|
# ─── Rejects: link-local, cloud-metadata, multicast, unspecified ────────────
|
|
|
|
|
|
def test_rejects_link_local():
|
|
with pytest.raises(ValueError, match="link-local|cloud metadata"):
|
|
# 169.254.169.254 is BOTH link-local AND cloud-metadata — both
|
|
# rejections are correct; we accept either message.
|
|
assert_safe_public_https_url("https://169.254.169.254/icon.png")
|
|
|
|
|
|
def test_rejects_cloud_metadata_alibaba():
|
|
with pytest.raises(ValueError, match="cloud metadata"):
|
|
assert_safe_public_https_url("https://100.100.100.200/icon.png")
|
|
|
|
|
|
def test_rejects_multicast():
|
|
with pytest.raises(ValueError, match="multicast"):
|
|
assert_safe_public_https_url("https://224.0.0.1/icon.png")
|
|
|
|
|
|
def test_rejects_unspecified():
|
|
with pytest.raises(ValueError, match="unspecified"):
|
|
assert_safe_public_https_url("https://0.0.0.0/icon.png")
|
|
|
|
|
|
# ─── IPv6 ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_rejects_ipv4_mapped_private():
|
|
# ::ffff:192.168.1.1 unwraps to 192.168.1.1 → private
|
|
with pytest.raises(ValueError, match="private"):
|
|
assert_safe_public_https_url("https://[::ffff:192.168.1.1]/icon.png")
|
|
|
|
|
|
def test_rejects_ipv4_mapped_cloud_metadata():
|
|
with pytest.raises(ValueError, match="cloud metadata|link-local"):
|
|
# ::ffff:169.254.169.254 unwraps and triggers cloud-metadata block
|
|
# (the cloud-metadata frozenset is checked first; link-local catches
|
|
# 169.254/16 if it slips through, hence the regex alternation).
|
|
assert_safe_public_https_url("https://[::ffff:169.254.169.254]/icon.png")
|
|
|
|
|
|
def test_rejects_ipv6_loopback():
|
|
with pytest.raises(ValueError, match="loopback"):
|
|
assert_safe_public_https_url("https://[::1]/icon.png")
|