Files
arc53--docsgpt/tests/core/test_url_validation.py
wehub-resource-sync fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

262 lines
9.9 KiB
Python

"""Tests for SSRF URL validation module."""
import pytest
from unittest.mock import patch
from application.core.url_validation import (
SSRFError,
validate_url,
validate_url_safe,
is_private_ip,
is_metadata_ip,
)
class TestIsPrivateIP:
"""Tests for is_private_ip function."""
def test_loopback_ipv4(self):
assert is_private_ip("127.0.0.1") is True
assert is_private_ip("127.255.255.255") is True
def test_private_class_a(self):
assert is_private_ip("10.0.0.1") is True
assert is_private_ip("10.255.255.255") is True
def test_private_class_b(self):
assert is_private_ip("172.16.0.1") is True
assert is_private_ip("172.31.255.255") is True
def test_private_class_c(self):
assert is_private_ip("192.168.0.1") is True
assert is_private_ip("192.168.255.255") is True
def test_link_local(self):
assert is_private_ip("169.254.0.1") is True
def test_public_ip(self):
assert is_private_ip("8.8.8.8") is False
assert is_private_ip("1.1.1.1") is False
assert is_private_ip("93.184.216.34") is False
def test_invalid_ip(self):
assert is_private_ip("not-an-ip") is False
assert is_private_ip("") is False
class TestIsMetadataIP:
"""Tests for is_metadata_ip function."""
def test_aws_metadata_ip(self):
assert is_metadata_ip("169.254.169.254") is True
def test_aws_ecs_metadata_ip(self):
assert is_metadata_ip("169.254.170.2") is True
def test_non_metadata_ip(self):
assert is_metadata_ip("8.8.8.8") is False
assert is_metadata_ip("10.0.0.1") is False
class TestValidateUrl:
"""Tests for validate_url function."""
def test_adds_scheme_if_missing(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "93.184.216.34" # Public IP
result = validate_url("example.com")
assert result == "http://example.com"
def test_preserves_https_scheme(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "93.184.216.34"
result = validate_url("https://example.com")
assert result == "https://example.com"
def test_blocks_localhost(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://localhost")
assert "localhost" in str(exc_info.value).lower()
def test_blocks_localhost_localdomain(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://localhost.localdomain")
assert "not allowed" in str(exc_info.value).lower()
def test_blocks_loopback_ip(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://127.0.0.1")
assert "private" in str(exc_info.value).lower() or "internal" in str(exc_info.value).lower()
def test_blocks_private_ip_class_a(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://10.0.0.1")
assert "private" in str(exc_info.value).lower() or "internal" in str(exc_info.value).lower()
def test_blocks_private_ip_class_b(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://172.16.0.1")
assert "private" in str(exc_info.value).lower() or "internal" in str(exc_info.value).lower()
def test_blocks_private_ip_class_c(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://192.168.1.1")
assert "private" in str(exc_info.value).lower() or "internal" in str(exc_info.value).lower()
def test_blocks_aws_metadata_ip(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://169.254.169.254")
assert "metadata" in str(exc_info.value).lower()
def test_blocks_aws_metadata_with_path(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://169.254.169.254/latest/meta-data/")
assert "metadata" in str(exc_info.value).lower()
def test_blocks_gcp_metadata_hostname(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://metadata.google.internal")
assert "not allowed" in str(exc_info.value).lower()
def test_blocks_ftp_scheme(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("ftp://example.com")
assert "scheme" in str(exc_info.value).lower()
def test_blocks_file_scheme(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("file:///etc/passwd")
assert "scheme" in str(exc_info.value).lower()
def test_blocks_hostname_resolving_to_private_ip(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "192.168.1.1"
with pytest.raises(SSRFError) as exc_info:
validate_url("http://internal.example.com")
assert "private" in str(exc_info.value).lower() or "internal" in str(exc_info.value).lower()
def test_blocks_hostname_resolving_to_metadata_ip(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "169.254.169.254"
with pytest.raises(SSRFError) as exc_info:
validate_url("http://evil.example.com")
assert "metadata" in str(exc_info.value).lower()
def test_allows_public_ip(self):
result = validate_url("http://8.8.8.8")
assert result == "http://8.8.8.8"
def test_allows_public_hostname(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "93.184.216.34"
result = validate_url("https://example.com")
assert result == "https://example.com"
def test_raises_on_unresolvable_hostname(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = None
with pytest.raises(SSRFError) as exc_info:
validate_url("http://nonexistent.invalid")
assert "resolve" in str(exc_info.value).lower()
def test_raises_on_empty_hostname(self):
with pytest.raises(SSRFError) as exc_info:
validate_url("http://")
assert "hostname" in str(exc_info.value).lower()
def test_allow_localhost_flag(self):
# Should work with allow_localhost=True
result = validate_url("http://localhost", allow_localhost=True)
assert result == "http://localhost"
result = validate_url("http://127.0.0.1", allow_localhost=True)
assert result == "http://127.0.0.1"
class TestValidateUrlSafe:
"""Tests for validate_url_safe non-throwing function."""
def test_returns_tuple_on_success(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "93.184.216.34"
is_valid, url, error = validate_url_safe("https://example.com")
assert is_valid is True
assert url == "https://example.com"
assert error is None
def test_returns_tuple_on_failure(self):
is_valid, url, error = validate_url_safe("http://localhost")
assert is_valid is False
assert url == "http://localhost"
assert error is not None
assert "localhost" in error.lower()
def test_returns_error_message_for_private_ip(self):
is_valid, url, error = validate_url_safe("http://192.168.1.1")
assert is_valid is False
assert "private" in error.lower() or "internal" in error.lower()
def test_adds_scheme_when_missing(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "93.184.216.34"
is_valid, url, error = validate_url_safe("example.com")
assert is_valid is True
assert url == "http://example.com"
class TestIsPrivateIPExtended:
"""Additional edge cases for IP classification."""
def test_multicast_ip(self):
assert is_private_ip("224.0.0.1") is True
def test_unspecified_ip(self):
assert is_private_ip("0.0.0.0") is True
def test_ipv6_loopback(self):
assert is_private_ip("::1") is True
def test_ipv6_private(self):
assert is_private_ip("fc00::1") is True
def test_ipv6_public(self):
assert is_private_ip("2607:f8b0:4004:800::200e") is False
def test_reserved_ip(self):
# 240.0.0.0/4 is reserved (future use), Python's ipaddress marks it as such
assert is_private_ip("240.0.0.1") is True
class TestValidateUrlExtended:
"""Additional URL validation tests."""
def test_blocks_metadata_hostname(self):
with pytest.raises(SSRFError):
validate_url("http://metadata")
def test_allows_localhost_with_flag(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "192.168.1.1"
result = validate_url(
"http://internal.local", allow_localhost=True
)
assert result == "http://internal.local"
def test_blocks_aws_ecs_metadata_ip(self):
with pytest.raises(SSRFError, match="metadata"):
validate_url("http://169.254.170.2")
def test_blocks_aws_ipv6_metadata(self):
with pytest.raises(SSRFError, match="metadata"):
validate_url("http://[fd00:ec2::254]")
def test_blocks_hostname_resolving_to_loopback(self):
with patch("application.core.url_validation.resolve_hostname") as mock_resolve:
mock_resolve.return_value = "127.0.0.1"
with pytest.raises(SSRFError):
validate_url("http://sneaky.example.com")
def test_allows_localhost_ip_with_flag(self):
result = validate_url("http://10.0.0.1", allow_localhost=True)
assert result == "http://10.0.0.1"