""" OLLAMA URL endpoint poisoning 防護測試 — Wave8-X2 + ADR-110 vuln #1:OLLAMA_URL / OLLAMA_FALLBACK_URL 缺 IP allowlist 校驗 修法:pydantic field_validator 拒絕非 private/loopback/known-hostname 2026-04-27 Wave8-X2 by Claude — vuln #1 + B14 + alerter memory dedup 2026-05-03 ogt: ADR-110 GCP 三層容災,更新 fixture 預設值為 GCP-A,新增 GCP 白名單測試 """ from __future__ import annotations import pytest from pydantic import ValidationError # ============================================================================= # Helpers # ============================================================================= def _make_settings(**kwargs): """建立 Settings 實例,只覆蓋指定欄位,其餘用安全預設值。""" from src.core.config import Settings base = { "DATABASE_URL": "postgresql://u:p@localhost:5432/test", "OLLAMA_URL": "http://34.143.170.20:11434", # GCP-A Primary(ADR-110 2026-05-03) "OLLAMA_FALLBACK_URL": "", } base.update(kwargs) return Settings(**base) # ============================================================================= # #1: 公網 IP 應被拒絕 # ============================================================================= def test_public_ip_rejected_in_ollama_url(): """8.8.8.8 是公網 IP,應被 validator 拒絕(端點中毒攻擊情境)""" with pytest.raises(ValidationError, match="公網"): _make_settings(OLLAMA_URL="http://8.8.8.8:11434") def test_public_ip_rejected_in_ollama_fallback_url(): """FALLBACK_URL 也受同一 validator 保護""" with pytest.raises(ValidationError, match="公網"): _make_settings(OLLAMA_FALLBACK_URL="http://1.1.1.1:11434") # ============================================================================= # #2: 外部域名應被拒絕 # ============================================================================= def test_external_domain_rejected(): """attacker.com 是外部域名(非 IP,非白名單),應被拒絕""" with pytest.raises(ValidationError, match="外部域名"): _make_settings(OLLAMA_URL="http://attacker.com:11434") def test_external_domain_fallback_rejected(): """FALLBACK_URL 外部域名也應被拒絕""" with pytest.raises(ValidationError, match="外部域名"): _make_settings(OLLAMA_FALLBACK_URL="http://evil.example.com:11434") # ============================================================================= # #3: GCP 白名單公網 IP 應通過(ADR-110 2026-05-03) # ============================================================================= def test_gcp_a_public_ip_accepted(): """34.143.170.20 是 GCP-A 核准公網 IP(ADR-110 白名單),應通過""" s = _make_settings(OLLAMA_URL="http://34.143.170.20:11434") assert s.OLLAMA_URL == "http://34.143.170.20:11434" def test_gcp_b_public_ip_accepted(): """34.21.145.224 是 GCP-B 核准公網 IP(ADR-110 白名單),應通過""" s = _make_settings(OLLAMA_URL="http://34.21.145.224:11434") assert s.OLLAMA_URL == "http://34.21.145.224:11434" def test_gcp_b_as_secondary_url_accepted(): """GCP-B 作為 OLLAMA_SECONDARY_URL 也應通過白名單""" s = _make_settings( OLLAMA_URL="http://34.143.170.20:11434", OLLAMA_SECONDARY_URL="http://34.21.145.224:11434", ) assert s.OLLAMA_SECONDARY_URL == "http://34.21.145.224:11434" def test_arbitrary_public_ip_rejected(): """8.8.8.8 非 GCP 白名單公網 IP,仍應被拒絕(端點中毒攻擊防護)""" with pytest.raises(ValidationError, match="公網"): _make_settings(OLLAMA_URL="http://8.8.8.8:11434") # ============================================================================= # #4: 私網 IP 應通過 # ============================================================================= def test_private_ip_192_168_accepted(): """192.168.0.111 是 RFC1918 私網 IP(Local HDD 後備主機),應通過""" s = _make_settings(OLLAMA_URL="http://192.168.0.111:11434") assert s.OLLAMA_URL == "http://192.168.0.111:11434" def test_private_ip_10_x_accepted(): """10.x.x.x 是 RFC1918 私網 IP,應通過""" s = _make_settings(OLLAMA_URL="http://10.0.0.5:11434") assert s.OLLAMA_URL == "http://10.0.0.5:11434" def test_private_ip_172_16_accepted(): """172.16.x.x 是 RFC1918 私網 IP,應通過""" s = _make_settings(OLLAMA_FALLBACK_URL="http://172.16.0.10:11434") assert s.OLLAMA_FALLBACK_URL == "http://172.16.0.10:11434" # ============================================================================= # #5: localhost / loopback 應通過 # ============================================================================= def test_localhost_accepted(): """localhost 在 known hostname 白名單,應通過""" s = _make_settings(OLLAMA_URL="http://localhost:11434") assert s.OLLAMA_URL == "http://localhost:11434" def test_loopback_ip_accepted(): """127.0.0.1 是 loopback IP,應通過""" s = _make_settings(OLLAMA_URL="http://127.0.0.1:11434") assert s.OLLAMA_URL == "http://127.0.0.1:11434" # ============================================================================= # #6: 已知 K8s Service hostname 應通過 # ============================================================================= def test_known_k8s_svc_ollama_svc_accepted(): """ollama-svc 在 K8s Service 白名單,應通過""" s = _make_settings(OLLAMA_URL="http://ollama-svc:11434") assert s.OLLAMA_URL == "http://ollama-svc:11434" def test_known_k8s_svc_ollama_fallback_svc_accepted(): """ollama-fallback-svc 在白名單,應通過""" s = _make_settings(OLLAMA_FALLBACK_URL="http://ollama-fallback-svc:11434") assert s.OLLAMA_FALLBACK_URL == "http://ollama-fallback-svc:11434" # ============================================================================= # #7: 空字串應通過(OLLAMA_FALLBACK_URL 預設值) # ============================================================================= def test_empty_string_fallback_url_accepted(): """OLLAMA_FALLBACK_URL 預設空字串(未設定),應通過""" s = _make_settings(OLLAMA_FALLBACK_URL="") assert s.OLLAMA_FALLBACK_URL == ""