Files
awoooi/apps/api/tests/test_config_url_validation.py
ogt 4561c65fe9
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 1m46s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
fix(api): cap db pool during prod rollout
2026-07-01 17:00:53 +08:00

175 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
OLLAMA URL endpoint poisoning 防護測試 — Wave8-X2 + ADR-110
vuln #1OLLAMA_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 PrimaryADR-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 核准公網 IPADR-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 核准公網 IPADR-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 私網 IPLocal 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 == ""
def test_database_pool_budget_defaults_to_production_safe_values():
"""Production DB role has a tiny connection limit; defaults must not fan out."""
s = _make_settings()
assert s.DATABASE_POOL_SIZE == 1
assert s.DATABASE_MAX_OVERFLOW == 0
assert s.DATABASE_POOL_TIMEOUT_SECONDS == 5.0