Skip to content

Commit 0b65c8b

Browse files
miss-islingtonsethmlarsongpshead
authored
[3.10] gh-122133: Authenticate socket connection for socket.socketpair() fallback (GH-122134) (#122427)
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 <seth@python.org> Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent d86ab5d commit 0b65c8b

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

Lib/test/test_socket.py

+125-3
Original file line numberDiff line numberDiff line change
@@ -557,19 +557,27 @@ class SocketPairTest(unittest.TestCase, ThreadableTest):
557557
def __init__(self, methodName='runTest'):
558558
unittest.TestCase.__init__(self, methodName=methodName)
559559
ThreadableTest.__init__(self)
560+
self.cli = None
561+
self.serv = None
562+
563+
def socketpair(self):
564+
# To be overridden by some child classes.
565+
return socket.socketpair()
560566

561567
def setUp(self):
562-
self.serv, self.cli = socket.socketpair()
568+
self.serv, self.cli = self.socketpair()
563569

564570
def tearDown(self):
565-
self.serv.close()
571+
if self.serv:
572+
self.serv.close()
566573
self.serv = None
567574

568575
def clientSetUp(self):
569576
pass
570577

571578
def clientTearDown(self):
572-
self.cli.close()
579+
if self.cli:
580+
self.cli.close()
573581
self.cli = None
574582
ThreadableTest.clientTearDown(self)
575583

@@ -4630,6 +4638,120 @@ def _testSend(self):
46304638
self.assertEqual(msg, MSG)
46314639

46324640

4641+
class PurePythonSocketPairTest(SocketPairTest):
4642+
4643+
# Explicitly use socketpair AF_INET or AF_INET6 to ensure that is the
4644+
# code path we're using regardless platform is the pure python one where
4645+
# `_socket.socketpair` does not exist. (AF_INET does not work with
4646+
# _socket.socketpair on many platforms).
4647+
def socketpair(self):
4648+
# called by super().setUp().
4649+
try:
4650+
return socket.socketpair(socket.AF_INET6)
4651+
except OSError:
4652+
return socket.socketpair(socket.AF_INET)
4653+
4654+
# Local imports in this class make for easy security fix backporting.
4655+
4656+
def setUp(self):
4657+
import _socket
4658+
self._orig_sp = getattr(_socket, 'socketpair', None)
4659+
if self._orig_sp is not None:
4660+
# This forces the version using the non-OS provided socketpair
4661+
# emulation via an AF_INET socket in Lib/socket.py.
4662+
del _socket.socketpair
4663+
import importlib
4664+
global socket
4665+
socket = importlib.reload(socket)
4666+
else:
4667+
pass # This platform already uses the non-OS provided version.
4668+
super().setUp()
4669+
4670+
def tearDown(self):
4671+
super().tearDown()
4672+
import _socket
4673+
if self._orig_sp is not None:
4674+
# Restore the default socket.socketpair definition.
4675+
_socket.socketpair = self._orig_sp
4676+
import importlib
4677+
global socket
4678+
socket = importlib.reload(socket)
4679+
4680+
def test_recv(self):
4681+
msg = self.serv.recv(1024)
4682+
self.assertEqual(msg, MSG)
4683+
4684+
def _test_recv(self):
4685+
self.cli.send(MSG)
4686+
4687+
def test_send(self):
4688+
self.serv.send(MSG)
4689+
4690+
def _test_send(self):
4691+
msg = self.cli.recv(1024)
4692+
self.assertEqual(msg, MSG)
4693+
4694+
def test_ipv4(self):
4695+
cli, srv = socket.socketpair(socket.AF_INET)
4696+
cli.close()
4697+
srv.close()
4698+
4699+
def _test_ipv4(self):
4700+
pass
4701+
4702+
@unittest.skipIf(not hasattr(_socket, 'IPPROTO_IPV6') or
4703+
not hasattr(_socket, 'IPV6_V6ONLY'),
4704+
"IPV6_V6ONLY option not supported")
4705+
@unittest.skipUnless(socket_helper.IPV6_ENABLED, 'IPv6 required for this test')
4706+
def test_ipv6(self):
4707+
cli, srv = socket.socketpair(socket.AF_INET6)
4708+
cli.close()
4709+
srv.close()
4710+
4711+
def _test_ipv6(self):
4712+
pass
4713+
4714+
def test_injected_authentication_failure(self):
4715+
orig_getsockname = socket.socket.getsockname
4716+
inject_sock = None
4717+
4718+
def inject_getsocketname(self):
4719+
nonlocal inject_sock
4720+
sockname = orig_getsockname(self)
4721+
# Connect to the listening socket ahead of the
4722+
# client socket.
4723+
if inject_sock is None:
4724+
inject_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
4725+
inject_sock.setblocking(False)
4726+
try:
4727+
inject_sock.connect(sockname[:2])
4728+
except (BlockingIOError, InterruptedError):
4729+
pass
4730+
inject_sock.setblocking(True)
4731+
return sockname
4732+
4733+
sock1 = sock2 = None
4734+
try:
4735+
socket.socket.getsockname = inject_getsocketname
4736+
with self.assertRaises(OSError):
4737+
sock1, sock2 = socket.socketpair()
4738+
finally:
4739+
socket.socket.getsockname = orig_getsockname
4740+
if inject_sock:
4741+
inject_sock.close()
4742+
if sock1: # This cleanup isn't needed on a successful test.
4743+
sock1.close()
4744+
if sock2:
4745+
sock2.close()
4746+
4747+
def _test_injected_authentication_failure(self):
4748+
# No-op. Exists for base class threading infrastructure to call.
4749+
# We could refactor this test into its own lesser class along with the
4750+
# setUp and tearDown code to construct an ideal; it is simpler to keep
4751+
# it here and live with extra overhead one this _one_ failure test.
4752+
pass
4753+
4754+
46334755
class NonBlockingTCPTests(ThreadedTCPSocketTest):
46344756

46354757
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 <greg@krypto.org> and Seth Larson <seth@python.org>. Reported by Ellie
5+
<el@horse64.org>

0 commit comments

Comments
 (0)