4cd2d4af2b
Test Browser Use CLI Install / uv pip install (ubuntu-latest) (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use from local wheel (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use[cli] from PyPI (push) Failing after 1s
package / pip-install-on-macos-latest-py-3.11 (push) Has been skipped
package / pip-install-on-macos-latest-py-3.13 (push) Has been skipped
package / pip-install-on-ubuntu-latest-py-3.11 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.13 (push) Has been skipped
cloud_evals / trigger_cloud_eval_image_build (push) Failing after 1s
docker / build_publish_image (push) Failing after 1s
Test Browser Use CLI Install / browser-use skill sync (push) Failing after 1s
lint / code-style (push) Failing after 0s
lint / type-checker (push) Failing after 1s
package / pip-build (push) Failing after 1s
lint / syntax-errors (push) Failing after 3s
package / pip-install-on-ubuntu-latest-py-3.13 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.11 (push) Has been skipped
test / ${{ matrix.test_filename }} (push) Has been skipped
test / evaluate-tasks (push) Has been skipped
test / setup-chromium (push) Failing after 2s
test / find_tests (push) Failing after 2s
Test Browser Use CLI Install / uv pip install (windows-latest) (push) Has been cancelled
Test Browser Use CLI Install / uv pip install (macos-latest) (push) Has been cancelled
676 lines
31 KiB
Python
676 lines
31 KiB
Python
"""
|
||
Comprehensive tests for IP address blocking in SecurityWatchdog.
|
||
|
||
Tests cover IPv4, IPv6, localhost, private networks, edge cases, and interactions
|
||
with allowed_domains and prohibited_domains configurations.
|
||
"""
|
||
|
||
from bubus import EventBus
|
||
|
||
from browser_use.browser import BrowserProfile, BrowserSession
|
||
from browser_use.browser.watchdogs.security_watchdog import SecurityWatchdog
|
||
|
||
|
||
class TestIPv4Blocking:
|
||
"""Test blocking of IPv4 addresses."""
|
||
|
||
def test_block_public_ipv4_addresses(self):
|
||
"""Test that public IPv4 addresses are blocked when block_ip_addresses=True."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Public IPv4 addresses should be blocked
|
||
assert watchdog._is_url_allowed('http://180.1.1.1/supersafe.txt') is False
|
||
assert watchdog._is_url_allowed('https://8.8.8.8/') is False
|
||
assert watchdog._is_url_allowed('http://1.1.1.1:8080/api') is False
|
||
assert watchdog._is_url_allowed('https://142.250.185.46/search') is False
|
||
assert watchdog._is_url_allowed('http://93.184.216.34/') is False
|
||
|
||
def test_block_private_ipv4_networks(self):
|
||
"""Test that private network IPv4 addresses are blocked."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Private network ranges (RFC 1918)
|
||
assert watchdog._is_url_allowed('http://192.168.1.1/') is False
|
||
assert watchdog._is_url_allowed('http://192.168.0.100/admin') is False
|
||
assert watchdog._is_url_allowed('http://10.0.0.1/') is False
|
||
assert watchdog._is_url_allowed('http://10.255.255.255/') is False
|
||
assert watchdog._is_url_allowed('http://172.16.0.1/') is False
|
||
assert watchdog._is_url_allowed('http://172.31.255.254/') is False
|
||
|
||
def test_block_localhost_ipv4(self):
|
||
"""Test that localhost IPv4 addresses are blocked."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Localhost/loopback addresses
|
||
assert watchdog._is_url_allowed('http://127.0.0.1/') is False
|
||
assert watchdog._is_url_allowed('http://127.0.0.1:8080/') is False
|
||
assert watchdog._is_url_allowed('https://127.0.0.1:3000/api/test') is False
|
||
assert watchdog._is_url_allowed('http://127.1.2.3/') is False # Any 127.x.x.x
|
||
|
||
def test_block_ipv4_with_ports_and_paths(self):
|
||
"""Test that IPv4 addresses with ports and paths are blocked."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# With various ports
|
||
assert watchdog._is_url_allowed('http://8.8.8.8:80/') is False
|
||
assert watchdog._is_url_allowed('https://8.8.8.8:443/') is False
|
||
assert watchdog._is_url_allowed('http://192.168.1.1:8080/') is False
|
||
assert watchdog._is_url_allowed('http://10.0.0.1:3000/api') is False
|
||
|
||
# With paths and query strings
|
||
assert watchdog._is_url_allowed('http://1.2.3.4/path/to/resource') is False
|
||
assert watchdog._is_url_allowed('http://5.6.7.8/api?key=value') is False
|
||
assert watchdog._is_url_allowed('https://9.10.11.12/path/to/file.html#anchor') is False
|
||
|
||
def test_allow_ipv4_when_blocking_disabled(self):
|
||
"""Test that IPv4 addresses are allowed when block_ip_addresses=False (default)."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=False, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# All IP addresses should be allowed when blocking is disabled
|
||
assert watchdog._is_url_allowed('http://180.1.1.1/supersafe.txt') is True
|
||
assert watchdog._is_url_allowed('http://192.168.1.1/') is True
|
||
assert watchdog._is_url_allowed('http://127.0.0.1:8080/') is True
|
||
assert watchdog._is_url_allowed('http://8.8.8.8/') is True
|
||
|
||
|
||
class TestIPv6Blocking:
|
||
"""Test blocking of IPv6 addresses."""
|
||
|
||
def test_block_ipv6_addresses(self):
|
||
"""Test that IPv6 addresses are blocked when block_ip_addresses=True."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Public IPv6 addresses (with brackets as per URL standard)
|
||
assert watchdog._is_url_allowed('http://[2001:db8::1]/') is False
|
||
assert watchdog._is_url_allowed('https://[2001:4860:4860::8888]/') is False
|
||
assert watchdog._is_url_allowed('http://[2606:4700:4700::1111]/path') is False
|
||
assert watchdog._is_url_allowed('https://[2001:db8:85a3::8a2e:370:7334]/api') is False
|
||
|
||
def test_block_ipv6_localhost(self):
|
||
"""Test that IPv6 localhost addresses are blocked."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# IPv6 loopback
|
||
assert watchdog._is_url_allowed('http://[::1]/') is False
|
||
assert watchdog._is_url_allowed('http://[::1]:8080/') is False
|
||
assert watchdog._is_url_allowed('https://[::1]:3000/api') is False
|
||
assert watchdog._is_url_allowed('http://[0:0:0:0:0:0:0:1]/') is False # Expanded form
|
||
|
||
def test_block_ipv6_with_ports_and_paths(self):
|
||
"""Test that IPv6 addresses with ports and paths are blocked."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# IPv6 with ports
|
||
assert watchdog._is_url_allowed('http://[2001:db8::1]:80/') is False
|
||
assert watchdog._is_url_allowed('https://[2001:db8::1]:443/') is False
|
||
assert watchdog._is_url_allowed('http://[::1]:8080/api') is False
|
||
|
||
# IPv6 with paths
|
||
assert watchdog._is_url_allowed('http://[2001:db8::1]/path/to/resource') is False
|
||
assert watchdog._is_url_allowed('https://[2001:db8::1]/api?key=value') is False
|
||
|
||
def test_allow_ipv6_when_blocking_disabled(self):
|
||
"""Test that IPv6 addresses are allowed when block_ip_addresses=False."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=False, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# All IPv6 addresses should be allowed
|
||
assert watchdog._is_url_allowed('http://[2001:db8::1]/') is True
|
||
assert watchdog._is_url_allowed('http://[::1]:8080/') is True
|
||
assert watchdog._is_url_allowed('https://[2001:4860:4860::8888]/') is True
|
||
|
||
|
||
class TestDomainNamesStillAllowed:
|
||
"""Test that regular domain names are not affected by IP blocking."""
|
||
|
||
def test_domain_names_allowed_with_ip_blocking(self):
|
||
"""Test that domain names continue to work when IP blocking is enabled."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Regular domain names should still be allowed
|
||
assert watchdog._is_url_allowed('https://example.com') is True
|
||
assert watchdog._is_url_allowed('https://www.google.com') is True
|
||
assert watchdog._is_url_allowed('http://subdomain.example.org/path') is True
|
||
assert watchdog._is_url_allowed('https://api.github.com/repos') is True
|
||
assert watchdog._is_url_allowed('http://localhost/') is True # "localhost" is a domain name, not IP
|
||
assert watchdog._is_url_allowed('http://localhost:8080/api') is True
|
||
|
||
def test_domains_with_numbers_allowed(self):
|
||
"""Test that domain names containing numbers are still allowed."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Domains with numbers (but not valid IP addresses)
|
||
assert watchdog._is_url_allowed('https://example123.com') is True
|
||
assert watchdog._is_url_allowed('https://123example.com') is True
|
||
assert watchdog._is_url_allowed('https://server1.example.com') is True
|
||
assert watchdog._is_url_allowed('http://web2.site.org') is True
|
||
|
||
|
||
class TestIPBlockingWithAllowedDomains:
|
||
"""Test interaction between IP blocking and allowed_domains."""
|
||
|
||
def test_ip_blocked_even_in_allowed_domains(self):
|
||
"""Test that IPs are blocked even if they're in allowed_domains list."""
|
||
# Note: It doesn't make sense to add IPs to allowed_domains, but if someone does,
|
||
# IP blocking should take precedence
|
||
browser_profile = BrowserProfile(
|
||
block_ip_addresses=True,
|
||
allowed_domains=['example.com', '192.168.1.1'], # IP in allowlist
|
||
headless=True,
|
||
user_data_dir=None,
|
||
)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# IP should be blocked despite being in allowed_domains
|
||
assert watchdog._is_url_allowed('http://192.168.1.1/') is False
|
||
|
||
# Regular domain should work as expected
|
||
assert watchdog._is_url_allowed('https://example.com') is True
|
||
|
||
# Other domains not in allowed_domains should be blocked
|
||
assert watchdog._is_url_allowed('https://other.com') is False
|
||
|
||
def test_allowed_domains_with_ip_blocking_enabled(self):
|
||
"""Test that allowed_domains works normally with IP blocking enabled."""
|
||
browser_profile = BrowserProfile(
|
||
block_ip_addresses=True, allowed_domains=['example.com', '*.google.com'], headless=True, user_data_dir=None
|
||
)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Allowed domains should work
|
||
assert watchdog._is_url_allowed('https://example.com') is True
|
||
assert watchdog._is_url_allowed('https://www.google.com') is True
|
||
|
||
# Not allowed domains should be blocked
|
||
assert watchdog._is_url_allowed('https://other.com') is False
|
||
|
||
# IPs should be blocked regardless
|
||
assert watchdog._is_url_allowed('http://8.8.8.8/') is False
|
||
assert watchdog._is_url_allowed('http://192.168.1.1/') is False
|
||
|
||
|
||
class TestIPBlockingWithProhibitedDomains:
|
||
"""Test interaction between IP blocking and prohibited_domains."""
|
||
|
||
def test_ip_blocked_regardless_of_prohibited_domains(self):
|
||
"""Test that IPs are blocked when IP blocking is on, independent of prohibited_domains."""
|
||
browser_profile = BrowserProfile(
|
||
block_ip_addresses=True, prohibited_domains=['example.com'], headless=True, user_data_dir=None
|
||
)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# IPs should be blocked due to IP blocking
|
||
assert watchdog._is_url_allowed('http://192.168.1.1/') is False
|
||
assert watchdog._is_url_allowed('http://8.8.8.8/') is False
|
||
|
||
# Prohibited domain should be blocked
|
||
assert watchdog._is_url_allowed('https://example.com') is False
|
||
|
||
# Other domains should be allowed
|
||
assert watchdog._is_url_allowed('https://other.com') is True
|
||
|
||
def test_prohibited_domains_without_ip_blocking(self):
|
||
"""Test that prohibited_domains works normally when IP blocking is disabled."""
|
||
browser_profile = BrowserProfile(
|
||
block_ip_addresses=False, prohibited_domains=['example.com', '8.8.8.8'], headless=True, user_data_dir=None
|
||
)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Prohibited domain should be blocked
|
||
assert watchdog._is_url_allowed('https://example.com') is False
|
||
|
||
# IP in prohibited list should be blocked (by prohibited_domains, not IP blocking)
|
||
assert watchdog._is_url_allowed('http://8.8.8.8/') is False
|
||
|
||
# Other IPs should be allowed (IP blocking is off)
|
||
assert watchdog._is_url_allowed('http://192.168.1.1/') is True
|
||
|
||
# Other domains should be allowed
|
||
assert watchdog._is_url_allowed('https://other.com') is True
|
||
|
||
|
||
class TestEdgeCases:
|
||
"""Test edge cases and invalid inputs."""
|
||
|
||
def test_invalid_urls_handled_gracefully(self):
|
||
"""Test that invalid URLs don't cause crashes."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Invalid URLs should return False
|
||
assert watchdog._is_url_allowed('not-a-url') is False
|
||
assert watchdog._is_url_allowed('') is False
|
||
assert watchdog._is_url_allowed('http://') is False
|
||
assert watchdog._is_url_allowed('://example.com') is False
|
||
|
||
def test_internal_browser_urls_allowed(self):
|
||
"""Test that internal browser URLs are still allowed with IP blocking."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Internal URLs should always be allowed
|
||
assert watchdog._is_url_allowed('about:blank') is True
|
||
assert watchdog._is_url_allowed('chrome://new-tab-page/') is True
|
||
assert watchdog._is_url_allowed('chrome://new-tab-page') is True
|
||
assert watchdog._is_url_allowed('chrome://newtab/') is True
|
||
|
||
def test_ipv4_lookalike_domains_allowed(self):
|
||
"""Test that strings that look like IPs but cannot be resolved as IPs by
|
||
the kernel/browser are still treated as domain names and allowed.
|
||
|
||
Note: short-form IPv4 strings such as `1.2.3` (which Chromium resolves
|
||
as `1.2.0.3`) ARE recognized as IPs — see TestNonStandardIPv4Representations.
|
||
"""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# These look like IPs but cannot resolve as IPv4 in any form the browser accepts:
|
||
# - 999.999.999.999: out of range for every octet
|
||
# - 1.2.3.4.5: too many octets
|
||
assert watchdog._is_url_allowed('http://999.999.999.999/') is True
|
||
assert watchdog._is_url_allowed('http://1.2.3.4.5/') is True
|
||
|
||
def test_different_schemes_with_ips(self):
|
||
"""Test that IP blocking works across different URL schemes."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# HTTP and HTTPS
|
||
assert watchdog._is_url_allowed('http://192.168.1.1/') is False
|
||
assert watchdog._is_url_allowed('https://192.168.1.1/') is False
|
||
|
||
# FTP (if browser supports it)
|
||
assert watchdog._is_url_allowed('ftp://192.168.1.1/') is False
|
||
|
||
# WebSocket (parsed as regular URL)
|
||
assert watchdog._is_url_allowed('ws://192.168.1.1:8080/') is False
|
||
assert watchdog._is_url_allowed('wss://192.168.1.1:8080/') is False
|
||
|
||
|
||
class TestIsIPAddressHelper:
|
||
"""Test the _is_ip_address helper method directly."""
|
||
|
||
def test_valid_ipv4_detection(self):
|
||
"""Test that valid IPv4 addresses are correctly detected."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Valid IPv4 addresses
|
||
assert watchdog._is_ip_address('127.0.0.1') is True
|
||
assert watchdog._is_ip_address('192.168.1.1') is True
|
||
assert watchdog._is_ip_address('8.8.8.8') is True
|
||
assert watchdog._is_ip_address('255.255.255.255') is True
|
||
assert watchdog._is_ip_address('0.0.0.0') is True
|
||
|
||
def test_valid_ipv6_detection(self):
|
||
"""Test that valid IPv6 addresses are correctly detected."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Valid IPv6 addresses (without brackets - those are URL-specific)
|
||
assert watchdog._is_ip_address('::1') is True
|
||
assert watchdog._is_ip_address('2001:db8::1') is True
|
||
assert watchdog._is_ip_address('2001:4860:4860::8888') is True
|
||
assert watchdog._is_ip_address('fe80::1') is True
|
||
assert watchdog._is_ip_address('2001:db8:85a3::8a2e:370:7334') is True
|
||
|
||
def test_invalid_ip_detection(self):
|
||
"""Test that non-IP strings are correctly identified as not IPs."""
|
||
browser_profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Domain names (not IPs)
|
||
assert watchdog._is_ip_address('example.com') is False
|
||
assert watchdog._is_ip_address('www.google.com') is False
|
||
assert watchdog._is_ip_address('localhost') is False
|
||
|
||
# Invalid IPs (rejected by both ipaddress and inet_aton)
|
||
assert watchdog._is_ip_address('999.999.999.999') is False
|
||
assert watchdog._is_ip_address('1.2.3.4.5') is False
|
||
assert watchdog._is_ip_address('not-an-ip') is False
|
||
assert watchdog._is_ip_address('') is False
|
||
|
||
# Short-form IPv4 strings (1.2.3 == 1.2.0.3) ARE valid IPs that
|
||
# browsers/kernel resolve — covered by TestNonStandardIPv4Representations.
|
||
|
||
# IPs with ports or paths (not valid for the helper - it only checks hostnames)
|
||
assert watchdog._is_ip_address('192.168.1.1:8080') is False
|
||
assert watchdog._is_ip_address('192.168.1.1/path') is False
|
||
|
||
|
||
class TestDefaultBehavior:
|
||
"""Test that default behavior (no IP blocking) is maintained."""
|
||
|
||
def test_default_block_ip_addresses_is_false(self):
|
||
"""Test that block_ip_addresses defaults to False."""
|
||
browser_profile = BrowserProfile(headless=True, user_data_dir=None)
|
||
|
||
# Default should be False
|
||
assert browser_profile.block_ip_addresses is False
|
||
|
||
def test_no_blocking_by_default(self):
|
||
"""Test that IPs are not blocked by default."""
|
||
browser_profile = BrowserProfile(headless=True, user_data_dir=None)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# All IPs should be allowed by default
|
||
assert watchdog._is_url_allowed('http://180.1.1.1/supersafe.txt') is True
|
||
assert watchdog._is_url_allowed('http://192.168.1.1/') is True
|
||
assert watchdog._is_url_allowed('http://127.0.0.1:8080/') is True
|
||
assert watchdog._is_url_allowed('http://[::1]/') is True
|
||
assert watchdog._is_url_allowed('https://8.8.8.8/') is True
|
||
|
||
|
||
class TestComplexScenarios:
|
||
"""Test complex real-world scenarios."""
|
||
|
||
def test_mixed_configuration_comprehensive(self):
|
||
"""Test a complex configuration with multiple security settings."""
|
||
browser_profile = BrowserProfile(
|
||
block_ip_addresses=True,
|
||
allowed_domains=['example.com', '*.google.com'],
|
||
prohibited_domains=['bad.example.com'], # Should be ignored when allowlist is set
|
||
headless=True,
|
||
user_data_dir=None,
|
||
)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Allowed domains should work
|
||
assert watchdog._is_url_allowed('https://example.com') is True
|
||
assert watchdog._is_url_allowed('https://www.google.com') is True
|
||
assert watchdog._is_url_allowed('https://mail.google.com') is True
|
||
|
||
# IPs should be blocked
|
||
assert watchdog._is_url_allowed('http://8.8.8.8/') is False
|
||
assert watchdog._is_url_allowed('http://192.168.1.1/') is False
|
||
|
||
# Domains not in allowlist should be blocked
|
||
assert watchdog._is_url_allowed('https://other.com') is False
|
||
|
||
def test_localhost_development_scenario(self):
|
||
"""Test typical local development scenario."""
|
||
# Developer wants to block external IPs but allow domain names
|
||
browser_profile = BrowserProfile(
|
||
block_ip_addresses=True,
|
||
headless=True,
|
||
user_data_dir=None, # No domain restrictions
|
||
)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Domain names should work (including localhost as a name)
|
||
assert watchdog._is_url_allowed('http://localhost:3000/') is True
|
||
assert watchdog._is_url_allowed('http://localhost:8080/api') is True
|
||
|
||
# But localhost IP should be blocked
|
||
assert watchdog._is_url_allowed('http://127.0.0.1:3000/') is False
|
||
|
||
# External domains should work
|
||
assert watchdog._is_url_allowed('https://api.example.com') is True
|
||
|
||
# External IPs should be blocked
|
||
assert watchdog._is_url_allowed('http://8.8.8.8/') is False
|
||
|
||
def test_security_hardening_scenario(self):
|
||
"""Test maximum security scenario with IP blocking and domain restrictions."""
|
||
browser_profile = BrowserProfile(
|
||
block_ip_addresses=True,
|
||
allowed_domains=['example.com', 'api.example.com'],
|
||
headless=True,
|
||
user_data_dir=None,
|
||
)
|
||
browser_session = BrowserSession(browser_profile=browser_profile)
|
||
event_bus = EventBus()
|
||
watchdog = SecurityWatchdog(browser_session=browser_session, event_bus=event_bus)
|
||
|
||
# Only specified domains allowed
|
||
assert watchdog._is_url_allowed('https://example.com') is True
|
||
assert watchdog._is_url_allowed('https://api.example.com') is True
|
||
|
||
# IPs blocked
|
||
assert watchdog._is_url_allowed('http://192.168.1.1/') is False
|
||
|
||
# Other domains blocked
|
||
assert watchdog._is_url_allowed('https://other.com') is False
|
||
|
||
# Even localhost blocked
|
||
assert watchdog._is_url_allowed('http://127.0.0.1/') is False
|
||
|
||
|
||
class TestNonStandardIPv4Representations:
|
||
"""Regression tests for GHSA-xrfv-gg9f-wwjp / GHSA-g27c-8gp4-28cv.
|
||
|
||
`ipaddress.ip_address()` only accepts the canonical dotted-quad form, but
|
||
Chromium also resolves decimal, hex, octal, and short-form IPv4 strings.
|
||
Without canonicalization, `block_ip_addresses=True` could be bypassed via:
|
||
|
||
http://2130706433/ → 127.0.0.1 (decimal)
|
||
http://0x7f000001/ → 127.0.0.1 (hex)
|
||
http://0177.0.0.1/ → 127.0.0.1 (octal)
|
||
http://127.1/ → 127.0.0.1 (short-form)
|
||
http://127.0.1/ → 127.0.0.1 (short-form)
|
||
"""
|
||
|
||
def _watchdog(self) -> SecurityWatchdog:
|
||
profile = BrowserProfile(block_ip_addresses=True, headless=True, user_data_dir=None)
|
||
session = BrowserSession(browser_profile=profile)
|
||
return SecurityWatchdog(browser_session=session, event_bus=EventBus())
|
||
|
||
def test_decimal_ipv4_blocked(self):
|
||
"""Decimal integer IPv4 strings (browser-accepted) must be blocked."""
|
||
watchdog = self._watchdog()
|
||
assert watchdog._is_url_allowed('http://2130706433/') is False # 127.0.0.1
|
||
assert watchdog._is_url_allowed('http://3232235521/') is False # 192.168.0.1
|
||
|
||
def test_hex_ipv4_blocked(self):
|
||
"""Hex IPv4 strings (browser-accepted) must be blocked."""
|
||
watchdog = self._watchdog()
|
||
assert watchdog._is_url_allowed('http://0x7f000001/') is False # 127.0.0.1
|
||
assert watchdog._is_url_allowed('http://0x7F.0x0.0x0.0x1/') is False
|
||
|
||
def test_octal_ipv4_blocked(self):
|
||
"""Octal IPv4 strings (browser-accepted) must be blocked."""
|
||
watchdog = self._watchdog()
|
||
assert watchdog._is_url_allowed('http://0177.0.0.1/') is False # 127.0.0.1
|
||
|
||
def test_short_form_ipv4_blocked(self):
|
||
"""Short-form IPv4 strings (browser-accepted) must be blocked."""
|
||
watchdog = self._watchdog()
|
||
assert watchdog._is_url_allowed('http://127.1/') is False
|
||
assert watchdog._is_url_allowed('http://127.0.1/') is False
|
||
assert watchdog._is_url_allowed('http://10.1/') is False
|
||
|
||
def test_lookalike_domains_still_allowed(self):
|
||
"""Hostnames that look IP-ish but are not (e.g. embedded labels) must
|
||
remain unaffected — only browser-resolvable IP forms should be blocked."""
|
||
watchdog = self._watchdog()
|
||
# Real domains, not IPs — should not be incorrectly blocked.
|
||
assert watchdog._is_url_allowed('http://127.0.0.1.evil.com/') is True
|
||
assert watchdog._is_url_allowed('http://2130706433.evil.com/') is True
|
||
assert watchdog._is_url_allowed('http://example.com/') is True
|
||
|
||
def test_non_standard_forms_allowed_when_blocking_disabled(self):
|
||
"""Without block_ip_addresses, non-standard IPv4 strings are not blocked."""
|
||
profile = BrowserProfile(block_ip_addresses=False, headless=True, user_data_dir=None)
|
||
session = BrowserSession(browser_profile=profile)
|
||
watchdog = SecurityWatchdog(browser_session=session, event_bus=EventBus())
|
||
assert watchdog._is_url_allowed('http://2130706433/') is True
|
||
assert watchdog._is_url_allowed('http://0x7f000001/') is True
|
||
|
||
def test_non_standard_forms_blocked_inside_allowed_domains(self):
|
||
"""block_ip_addresses must override allowed_domains for non-standard forms
|
||
just as it does for the canonical form."""
|
||
profile = BrowserProfile(
|
||
block_ip_addresses=True,
|
||
allowed_domains=['*'],
|
||
headless=True,
|
||
user_data_dir=None,
|
||
)
|
||
session = BrowserSession(browser_profile=profile)
|
||
watchdog = SecurityWatchdog(browser_session=session, event_bus=EventBus())
|
||
# Even with '*' allowlist, the IP block must still fire.
|
||
assert watchdog._is_url_allowed('http://2130706433/') is False
|
||
assert watchdog._is_url_allowed('http://0x7f000001/') is False
|
||
# Sanity: legitimate domains still allowed.
|
||
assert watchdog._is_url_allowed('https://example.com/') is True
|
||
|
||
def test_malformed_unicode_hostnames_do_not_crash_classifier(self):
|
||
"""Hostnames with lone surrogates (e.g. `\\udcff` from URL-decoded
|
||
malformed UTF-8) must not crash `_is_ip_address` — `socket.inet_aton`
|
||
raises `UnicodeEncodeError` (not `OSError`) for surrogates, and the
|
||
classifier must treat that as "not an IP" rather than propagating.
|
||
|
||
Pre-existing defensive behavior in the original code (caught
|
||
`Exception`) — must be preserved when extending with `inet_aton`.
|
||
"""
|
||
watchdog = self._watchdog()
|
||
# Lone surrogates — common in URLs containing percent-encoded malformed UTF-8.
|
||
assert watchdog._is_ip_address('\udcff') is False
|
||
assert watchdog._is_ip_address('\ud800') is False
|
||
assert watchdog._is_ip_address('caf\udce9.local') is False
|
||
assert watchdog._is_ip_address('\udcff.example.com') is False
|
||
|
||
def test_percent_encoded_ipv4_blocked(self):
|
||
"""Percent-encoded hostnames that decode to IPs must be blocked.
|
||
|
||
Chromium percent-decodes the host before resolving — so
|
||
`http://%30x7f000001/` decodes to `0x7f000001` → `127.0.0.1`, and
|
||
`http://%31%32%37.0.0.1/` decodes to `127.0.0.1`. Without
|
||
percent-decoding inside the classifier, the IP block is bypassed
|
||
whenever the URL contains any `%`-encoded host bytes.
|
||
"""
|
||
watchdog = self._watchdog()
|
||
# Mixed encoding: %30 = '0', rest literal → '0x7f000001'
|
||
assert watchdog._is_url_allowed('http://%30x7f000001/') is False
|
||
# Fully encoded canonical form: %31%32%37 = '127'
|
||
assert watchdog._is_url_allowed('http://%31%32%37.0.0.1/') is False
|
||
# Fully encoded decimal form: → '2130706433'
|
||
assert watchdog._is_url_allowed('http://%32%31%33%30%37%30%36%34%33%33/') is False
|
||
# Direct classifier checks for the same decoded forms.
|
||
assert watchdog._is_ip_address('%30x7f000001') is True
|
||
assert watchdog._is_ip_address('%31%32%37.0.0.1') is True
|
||
assert watchdog._is_ip_address('%32%31%33%30%37%30%36%34%33%33') is True
|
||
|
||
def test_malformed_percent_encoding_does_not_crash(self):
|
||
"""Hostnames with malformed `%` escapes must not crash the classifier."""
|
||
watchdog = self._watchdog()
|
||
# `unquote` leaves bad `%`-sequences as-is; we must still treat the
|
||
# result as a non-IP rather than blowing up.
|
||
assert watchdog._is_ip_address('%') is False
|
||
assert watchdog._is_ip_address('%zz') is False
|
||
assert watchdog._is_ip_address('%2') is False
|
||
|
||
def test_unicode_normalized_ipv4_blocked(self):
|
||
"""Hostnames using fullwidth, circled, or other Unicode digit variants
|
||
that NFKC/IDNA-normalize to ASCII IPv4 literals must be blocked.
|
||
|
||
WHATWG URL canonicalization maps `127.0.0.1` (fullwidth digits),
|
||
`0x7f000001` (fullwidth zero + ASCII hex), and `①②⑦.⓪.⓪.①` (circled
|
||
digits) all to `127.0.0.1`. Without NFKC normalization in the
|
||
classifier, the new non-standard IPv4 blocking can be bypassed with
|
||
any equivalent Unicode digit form.
|
||
"""
|
||
watchdog = self._watchdog()
|
||
# Fullwidth digits (U+FF10..U+FF19) — NFKC → ASCII digits.
|
||
assert watchdog._is_url_allowed('http://127.0.0.1/') is False
|
||
# Fullwidth zero + ASCII hex 7f000001.
|
||
assert watchdog._is_url_allowed('http://0x7f000001/') is False
|
||
# Circled digits (U+2460+, U+24EA for zero).
|
||
assert watchdog._is_url_allowed('http://①②⑦.⓪.⓪.①/') is False
|
||
# Direct classifier checks.
|
||
assert watchdog._is_ip_address('127.0.0.1') is True
|
||
assert watchdog._is_ip_address('0x7f000001') is True
|
||
assert watchdog._is_ip_address('①②⑦.⓪.⓪.①') is True
|
||
|
||
def test_idn_domains_not_misclassified_as_ip(self):
|
||
"""Defense against false positives from the new normalization step:
|
||
legitimate IDN domains (Unicode letters / punycode) MUST NOT be
|
||
classified as IPs after NFKC."""
|
||
watchdog = self._watchdog()
|
||
assert watchdog._is_ip_address('café.example') is False
|
||
assert watchdog._is_ip_address('xn--caf-dma.example') is False
|
||
assert watchdog._is_ip_address('日本.example') is False
|
||
assert watchdog._is_ip_address('xn--wgv71a.example') is False
|
||
|
||
def test_idna_dot_separators_blocked(self):
|
||
"""Per RFC 3490 / UTS46, four code points act as label separators in
|
||
IDNA processing — `.` (U+002E), `。` (U+3002 IDEOGRAPHIC FULL STOP),
|
||
`.` (U+FF0E FULLWIDTH FULL STOP), `。` (U+FF61 HALFWIDTH IDEOGRAPHIC
|
||
FULL STOP). WHATWG URL parsing maps all four to `.` before resolution,
|
||
so `http://127。0。0。1/` etc. reach 127.0.0.1.
|
||
|
||
NFKC alone is insufficient — it maps U+FF0E → U+002E and U+FF61 →
|
||
U+3002, but leaves U+3002 (the most common one) untouched. Classifier
|
||
must additionally fold U+3002 and U+FF61 to U+002E before IP parsing.
|
||
"""
|
||
watchdog = self._watchdog()
|
||
# All four dot variants must result in the IP being blocked.
|
||
assert watchdog._is_url_allowed('http://127。0。0。1/') is False # U+3002
|
||
assert watchdog._is_url_allowed('http://127。0。0。1/') is False # U+FF61
|
||
assert watchdog._is_url_allowed('http://127.0.0.1/') is False # U+FF0E
|
||
# Combined with circled-digit normalization.
|
||
assert watchdog._is_url_allowed('http://①②⑦。⓪。⓪。①/') is False
|
||
# Direct classifier checks.
|
||
assert watchdog._is_ip_address('127。0。0。1') is True
|
||
assert watchdog._is_ip_address('127。0。0。1') is True
|
||
assert watchdog._is_ip_address('127.0.0.1') is True
|