Skip to content

Commit c43b26d

Browse files
authored
gh-115197: Stop resolving host in urllib.request proxy bypass (GH-115210)
Use of a proxy is intended to defer DNS for the hosts to the proxy itself, rather than a potential for information leak of the host doing DNS resolution itself for any reason. Proxy bypass lists are strictly name based. Most implementations of proxy support agree.
1 parent 6c1c94d commit c43b26d

File tree

3 files changed

+64
-44
lines changed

3 files changed

+64
-44
lines changed

Lib/test/test_urllib2.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
import subprocess
1616

1717
import urllib.request
18-
# The proxy bypass method imported below has logic specific to the OSX
19-
# proxy config data structure but is testable on all platforms.
18+
# The proxy bypass method imported below has logic specific to the
19+
# corresponding system but is testable on all platforms.
2020
from urllib.request import (Request, OpenerDirector, HTTPBasicAuthHandler,
2121
HTTPPasswordMgrWithPriorAuth, _parse_proxy,
22+
_proxy_bypass_winreg_override,
2223
_proxy_bypass_macosx_sysconf,
2324
AbstractDigestAuthHandler)
2425
from urllib.parse import urlparse
@@ -1485,6 +1486,30 @@ def test_proxy_https_proxy_authorization(self):
14851486
self.assertEqual(req.host, "proxy.example.com:3128")
14861487
self.assertEqual(req.get_header("Proxy-authorization"), "FooBar")
14871488

1489+
@unittest.skipUnless(os.name == "nt", "only relevant for Windows")
1490+
def test_winreg_proxy_bypass(self):
1491+
proxy_override = "www.example.com;*.example.net; 192.168.0.1"
1492+
proxy_bypass = _proxy_bypass_winreg_override
1493+
for host in ("www.example.com", "www.example.net", "192.168.0.1"):
1494+
self.assertTrue(proxy_bypass(host, proxy_override),
1495+
"expected bypass of %s to be true" % host)
1496+
1497+
for host in ("example.com", "www.example.org", "example.net",
1498+
"192.168.0.2"):
1499+
self.assertFalse(proxy_bypass(host, proxy_override),
1500+
"expected bypass of %s to be False" % host)
1501+
1502+
# check intranet address bypass
1503+
proxy_override = "example.com; <local>"
1504+
self.assertTrue(proxy_bypass("example.com", proxy_override),
1505+
"expected bypass of %s to be true" % host)
1506+
self.assertFalse(proxy_bypass("example.net", proxy_override),
1507+
"expected bypass of %s to be False" % host)
1508+
for host in ("test", "localhost"):
1509+
self.assertTrue(proxy_bypass(host, proxy_override),
1510+
"expect <local> to bypass intranet address '%s'"
1511+
% host)
1512+
14881513
@unittest.skipUnless(sys.platform == 'darwin', "only relevant for OSX")
14891514
def test_osx_proxy_bypass(self):
14901515
bypass = {

Lib/urllib/request.py

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2563,6 +2563,7 @@ def _proxy_bypass_macosx_sysconf(host, proxy_settings):
25632563
}
25642564
"""
25652565
from fnmatch import fnmatch
2566+
from ipaddress import AddressValueError, IPv4Address
25662567

25672568
hostonly, port = _splitport(host)
25682569

@@ -2579,20 +2580,17 @@ def ip2num(ipAddr):
25792580
return True
25802581

25812582
hostIP = None
2583+
try:
2584+
hostIP = int(IPv4Address(hostonly))
2585+
except AddressValueError:
2586+
pass
25822587

25832588
for value in proxy_settings.get('exceptions', ()):
25842589
# Items in the list are strings like these: *.local, 169.254/16
25852590
if not value: continue
25862591

25872592
m = re.match(r"(\d+(?:\.\d+)*)(/\d+)?", value)
2588-
if m is not None:
2589-
if hostIP is None:
2590-
try:
2591-
hostIP = socket.gethostbyname(hostonly)
2592-
hostIP = ip2num(hostIP)
2593-
except OSError:
2594-
continue
2595-
2593+
if m is not None and hostIP is not None:
25962594
base = ip2num(m.group(1))
25972595
mask = m.group(2)
25982596
if mask is None:
@@ -2615,6 +2613,31 @@ def ip2num(ipAddr):
26152613
return False
26162614

26172615

2616+
# Same as _proxy_bypass_macosx_sysconf, testable on all platforms
2617+
def _proxy_bypass_winreg_override(host, override):
2618+
"""Return True if the host should bypass the proxy server.
2619+
2620+
The proxy override list is obtained from the Windows
2621+
Internet settings proxy override registry value.
2622+
2623+
An example of a proxy override value is:
2624+
"www.example.com;*.example.net; 192.168.0.1"
2625+
"""
2626+
from fnmatch import fnmatch
2627+
2628+
host, _ = _splitport(host)
2629+
proxy_override = override.split(';')
2630+
for test in proxy_override:
2631+
test = test.strip()
2632+
# "<local>" should bypass the proxy server for all intranet addresses
2633+
if test == '<local>':
2634+
if '.' not in host:
2635+
return True
2636+
elif fnmatch(host, test):
2637+
return True
2638+
return False
2639+
2640+
26182641
if sys.platform == 'darwin':
26192642
from _scproxy import _get_proxy_settings, _get_proxies
26202643

@@ -2713,7 +2736,7 @@ def proxy_bypass_registry(host):
27132736
import winreg
27142737
except ImportError:
27152738
# Std modules, so should be around - but you never know!
2716-
return 0
2739+
return False
27172740
try:
27182741
internetSettings = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
27192742
r'Software\Microsoft\Windows\CurrentVersion\Internet Settings')
@@ -2723,40 +2746,10 @@ def proxy_bypass_registry(host):
27232746
'ProxyOverride')[0])
27242747
# ^^^^ Returned as Unicode but problems if not converted to ASCII
27252748
except OSError:
2726-
return 0
2749+
return False
27272750
if not proxyEnable or not proxyOverride:
2728-
return 0
2729-
# try to make a host list from name and IP address.
2730-
rawHost, port = _splitport(host)
2731-
host = [rawHost]
2732-
try:
2733-
addr = socket.gethostbyname(rawHost)
2734-
if addr != rawHost:
2735-
host.append(addr)
2736-
except OSError:
2737-
pass
2738-
try:
2739-
fqdn = socket.getfqdn(rawHost)
2740-
if fqdn != rawHost:
2741-
host.append(fqdn)
2742-
except OSError:
2743-
pass
2744-
# make a check value list from the registry entry: replace the
2745-
# '<local>' string by the localhost entry and the corresponding
2746-
# canonical entry.
2747-
proxyOverride = proxyOverride.split(';')
2748-
# now check if we match one of the registry values.
2749-
for test in proxyOverride:
2750-
if test == '<local>':
2751-
if '.' not in rawHost:
2752-
return 1
2753-
test = test.replace(".", r"\.") # mask dots
2754-
test = test.replace("*", r".*") # change glob sequence
2755-
test = test.replace("?", r".") # change glob char
2756-
for val in host:
2757-
if re.match(test, val, re.I):
2758-
return 1
2759-
return 0
2751+
return False
2752+
return _proxy_bypass_winreg_override(host, proxyOverride)
27602753

27612754
def proxy_bypass(host):
27622755
"""Return True, if host should be bypassed.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
``urllib.request`` no longer resolves the hostname before checking it
2+
against the system's proxy bypass list on macOS and Windows.

0 commit comments

Comments
 (0)