Skip to content

Commit 5f90aba

Browse files
miss-islingtonsethmlarsongpshead
authored
[3.11] gh-122133: Authenticate socket connection for socket.socketpair() fallback (GH-122134) (#122426)
Authenticate socket connection for `socket.socketpair()` fallback when the platform does not have a native `socketpair` C API. We authenticate in-process using `getsocketname` and `getpeername` (thanks to Nathaniel J Smith for that suggestion). (cherry picked from commit 78df104) Co-authored-by: Seth Michael Larson <[email protected]> Co-authored-by: Gregory P. Smith <[email protected]>
1 parent d542a9b commit 5f90aba

File tree

3 files changed

+147
-3
lines changed

3 files changed

+147
-3
lines changed

Lib/socket.py

+17
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,23 @@ def socketpair(family=AF_INET, type=SOCK_STREAM, proto=0):
648648
raise
649649
finally:
650650
lsock.close()
651+
652+
# Authenticating avoids using a connection from something else
653+
# able to connect to {host}:{port} instead of us.
654+
# We expect only AF_INET and AF_INET6 families.
655+
try:
656+
if (
657+
ssock.getsockname() != csock.getpeername()
658+
or csock.getsockname() != ssock.getpeername()
659+
):
660+
raise ConnectionError("Unexpected peer connection")
661+
except:
662+
# getsockname() and getpeername() can fail
663+
# if either socket isn't connected.
664+
ssock.close()
665+
csock.close()
666+
raise
667+
651668
return (ssock, csock)
652669
__all__.append("socketpair")
653670

Lib/test/test_socket.py

+125-3
Original file line numberDiff line numberDiff line change
@@ -542,19 +542,27 @@ class SocketPairTest(unittest.TestCase, ThreadableTest):
542542
def __init__(self, methodName='runTest'):
543543
unittest.TestCase.__init__(self, methodName=methodName)
544544
ThreadableTest.__init__(self)
545+
self.cli = None
546+
self.serv = None
547+
548+
def socketpair(self):
549+
# To be overridden by some child classes.
550+
return socket.socketpair()
545551

546552
def setUp(self):
547-
self.serv, self.cli = socket.socketpair()
553+
self.serv, self.cli = self.socketpair()
548554

549555
def tearDown(self):
550-
self.serv.close()
556+
if self.serv:
557+
self.serv.close()
551558
self.serv = None
552559

553560
def clientSetUp(self):
554561
pass
555562

556563
def clientTearDown(self):
557-
self.cli.close()
564+
if self.cli:
565+
self.cli.close()
558566
self.cli = None
559567
ThreadableTest.clientTearDown(self)
560568

@@ -4667,6 +4675,120 @@ def _testSend(self):
46674675
self.assertEqual(msg, MSG)
46684676

46694677

4678+
class PurePythonSocketPairTest(SocketPairTest):
4679+
4680+
# Explicitly use socketpair AF_INET or AF_INET6 to ensure that is the
4681+
# code path we're using regardless platform is the pure python one where
4682+
# `_socket.socketpair` does not exist. (AF_INET does not work with
4683+
# _socket.socketpair on many platforms).
4684+
def socketpair(self):
4685+
# called by super().setUp().
4686+
try:
4687+
return socket.socketpair(socket.AF_INET6)
4688+
except OSError:
4689+
return socket.socketpair(socket.AF_INET)
4690+
4691+
# Local imports in this class make for easy security fix backporting.
4692+
4693+
def setUp(self):
4694+
import _socket
4695+
self._orig_sp = getattr(_socket, 'socketpair', None)
4696+
if self._orig_sp is not None:
4697+
# This forces the version using the non-OS provided socketpair
4698+
# emulation via an AF_INET socket in Lib/socket.py.
4699+
del _socket.socketpair
4700+
import importlib
4701+
global socket
4702+
socket = importlib.reload(socket)
4703+
else:
4704+
pass # This platform already uses the non-OS provided version.
4705+
super().setUp()
4706+
4707+
def tearDown(self):
4708+
super().tearDown()
4709+
import _socket
4710+
if self._orig_sp is not None:
4711+
# Restore the default socket.socketpair definition.
4712+
_socket.socketpair = self._orig_sp
4713+
import importlib
4714+
global socket
4715+
socket = importlib.reload(socket)
4716+
4717+
def test_recv(self):
4718+
msg = self.serv.recv(1024)
4719+
self.assertEqual(msg, MSG)
4720+
4721+
def _test_recv(self):
4722+
self.cli.send(MSG)
4723+
4724+
def test_send(self):
4725+
self.serv.send(MSG)
4726+
4727+
def _test_send(self):
4728+
msg = self.cli.recv(1024)
4729+
self.assertEqual(msg, MSG)
4730+
4731+
def test_ipv4(self):
4732+
cli, srv = socket.socketpair(socket.AF_INET)
4733+
cli.close()
4734+
srv.close()
4735+
4736+
def _test_ipv4(self):
4737+
pass
4738+
4739+
@unittest.skipIf(not hasattr(_socket, 'IPPROTO_IPV6') or
4740+
not hasattr(_socket, 'IPV6_V6ONLY'),
4741+
"IPV6_V6ONLY option not supported")
4742+
@unittest.skipUnless(socket_helper.IPV6_ENABLED, 'IPv6 required for this test')
4743+
def test_ipv6(self):
4744+
cli, srv = socket.socketpair(socket.AF_INET6)
4745+
cli.close()
4746+
srv.close()
4747+
4748+
def _test_ipv6(self):
4749+
pass
4750+
4751+
def test_injected_authentication_failure(self):
4752+
orig_getsockname = socket.socket.getsockname
4753+
inject_sock = None
4754+
4755+
def inject_getsocketname(self):
4756+
nonlocal inject_sock
4757+
sockname = orig_getsockname(self)
4758+
# Connect to the listening socket ahead of the
4759+
# client socket.
4760+
if inject_sock is None:
4761+
inject_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
4762+
inject_sock.setblocking(False)
4763+
try:
4764+
inject_sock.connect(sockname[:2])
4765+
except (BlockingIOError, InterruptedError):
4766+
pass
4767+
inject_sock.setblocking(True)
4768+
return sockname
4769+
4770+
sock1 = sock2 = None
4771+
try:
4772+
socket.socket.getsockname = inject_getsocketname
4773+
with self.assertRaises(OSError):
4774+
sock1, sock2 = socket.socketpair()
4775+
finally:
4776+
socket.socket.getsockname = orig_getsockname
4777+
if inject_sock:
4778+
inject_sock.close()
4779+
if sock1: # This cleanup isn't needed on a successful test.
4780+
sock1.close()
4781+
if sock2:
4782+
sock2.close()
4783+
4784+
def _test_injected_authentication_failure(self):
4785+
# No-op. Exists for base class threading infrastructure to call.
4786+
# We could refactor this test into its own lesser class along with the
4787+
# setUp and tearDown code to construct an ideal; it is simpler to keep
4788+
# it here and live with extra overhead one this _one_ failure test.
4789+
pass
4790+
4791+
46704792
class NonBlockingTCPTests(ThreadedTCPSocketTest):
46714793

46724794
def __init__(self, methodName='runTest'):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Authenticate the socket connection for the ``socket.socketpair()`` fallback
2+
on platforms where ``AF_UNIX`` is not available like Windows.
3+
4+
Patch by Gregory P. Smith <[email protected]> and Seth Larson <[email protected]>. Reported by Ellie
5+

0 commit comments

Comments
 (0)