Skip to content

Commit 41be376

Browse files
[3.8] gh-115197: Stop resolving host in urllib.request proxy bypass (GH-115210) (GH-116069)
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. (cherry picked from commit c43b26d) Co-authored-by: Weii Wang <weii.wang@canonical.com>
1 parent 854f645 commit 41be376

File tree

3 files changed

+64
-44
lines changed

3 files changed

+64
-44
lines changed

Lib/test/test_urllib2.py

+27-2
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
import subprocess
1212

1313
import urllib.request
14-
# The proxy bypass method imported below has logic specific to the OSX
15-
# proxy config data structure but is testable on all platforms.
14+
# The proxy bypass method imported below has logic specific to the
15+
# corresponding system but is testable on all platforms.
1616
from urllib.request import (Request, OpenerDirector, HTTPBasicAuthHandler,
1717
HTTPPasswordMgrWithPriorAuth, _parse_proxy,
18+
_proxy_bypass_winreg_override,
1819
_proxy_bypass_macosx_sysconf,
1920
AbstractDigestAuthHandler)
2021
from urllib.parse import urlparse
@@ -1438,6 +1439,30 @@ def test_proxy_https_proxy_authorization(self):
14381439
self.assertEqual(req.host, "proxy.example.com:3128")
14391440
self.assertEqual(req.get_header("Proxy-authorization"), "FooBar")
14401441

1442+
@unittest.skipUnless(os.name == "nt", "only relevant for Windows")
1443+
def test_winreg_proxy_bypass(self):
1444+
proxy_override = "www.example.com;*.example.net; 192.168.0.1"
1445+
proxy_bypass = _proxy_bypass_winreg_override
1446+
for host in ("www.example.com", "www.example.net", "192.168.0.1"):
1447+
self.assertTrue(proxy_bypass(host, proxy_override),
1448+
"expected bypass of %s to be true" % host)
1449+
1450+
for host in ("example.com", "www.example.org", "example.net",
1451+
"192.168.0.2"):
1452+
self.assertFalse(proxy_bypass(host, proxy_override),
1453+
"expected bypass of %s to be False" % host)
1454+
1455+
# check intranet address bypass
1456+
proxy_override = "example.com; <local>"
1457+
self.assertTrue(proxy_bypass("example.com", proxy_override),
1458+
"expected bypass of %s to be true" % host)
1459+
self.assertFalse(proxy_bypass("example.net", proxy_override),
1460+
"expected bypass of %s to be False" % host)
1461+
for host in ("test", "localhost"):
1462+
self.assertTrue(proxy_bypass(host, proxy_override),
1463+
"expect <local> to bypass intranet address '%s'"
1464+
% host)
1465+
14411466
@unittest.skipUnless(sys.platform == 'darwin', "only relevant for OSX")
14421467
def test_osx_proxy_bypass(self):
14431468
bypass = {

Lib/urllib/request.py

+35-42
Original file line numberDiff line numberDiff line change
@@ -2572,6 +2572,7 @@ def _proxy_bypass_macosx_sysconf(host, proxy_settings):
25722572
}
25732573
"""
25742574
from fnmatch import fnmatch
2575+
from ipaddress import AddressValueError, IPv4Address
25752576

25762577
hostonly, port = _splitport(host)
25772578

@@ -2588,20 +2589,17 @@ def ip2num(ipAddr):
25882589
return True
25892590

25902591
hostIP = None
2592+
try:
2593+
hostIP = int(IPv4Address(hostonly))
2594+
except AddressValueError:
2595+
pass
25912596

25922597
for value in proxy_settings.get('exceptions', ()):
25932598
# Items in the list are strings like these: *.local, 169.254/16
25942599
if not value: continue
25952600

25962601
m = re.match(r"(\d+(?:\.\d+)*)(/\d+)?", value)
2597-
if m is not None:
2598-
if hostIP is None:
2599-
try:
2600-
hostIP = socket.gethostbyname(hostonly)
2601-
hostIP = ip2num(hostIP)
2602-
except OSError:
2603-
continue
2604-
2602+
if m is not None and hostIP is not None:
26052603
base = ip2num(m.group(1))
26062604
mask = m.group(2)
26072605
if mask is None:
@@ -2624,6 +2622,31 @@ def ip2num(ipAddr):
26242622
return False
26252623

26262624

2625+
# Same as _proxy_bypass_macosx_sysconf, testable on all platforms
2626+
def _proxy_bypass_winreg_override(host, override):
2627+
"""Return True if the host should bypass the proxy server.
2628+
2629+
The proxy override list is obtained from the Windows
2630+
Internet settings proxy override registry value.
2631+
2632+
An example of a proxy override value is:
2633+
"www.example.com;*.example.net; 192.168.0.1"
2634+
"""
2635+
from fnmatch import fnmatch
2636+
2637+
host, _ = _splitport(host)
2638+
proxy_override = override.split(';')
2639+
for test in proxy_override:
2640+
test = test.strip()
2641+
# "<local>" should bypass the proxy server for all intranet addresses
2642+
if test == '<local>':
2643+
if '.' not in host:
2644+
return True
2645+
elif fnmatch(host, test):
2646+
return True
2647+
return False
2648+
2649+
26272650
if sys.platform == 'darwin':
26282651
from _scproxy import _get_proxy_settings, _get_proxies
26292652

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

27662759
def proxy_bypass(host):
27672760
"""Return True, if host should be bypassed.
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)