Skip to content

Commit 220e31a

Browse files
miss-islingtonsethmlarsongpshead
authored
[3.12] gh-122133: Authenticate socket connection for socket.socketpair() fallback (GH-122134) (GH-122425)
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 bad8497 commit 220e31a

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

Lib/test/test_socket.py

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

562568
def setUp(self):
563-
self.serv, self.cli = socket.socketpair()
569+
self.serv, self.cli = self.socketpair()
564570

565571
def tearDown(self):
566-
self.serv.close()
572+
if self.serv:
573+
self.serv.close()
567574
self.serv = None
568575

569576
def clientSetUp(self):
570577
pass
571578

572579
def clientTearDown(self):
573-
self.cli.close()
580+
if self.cli:
581+
self.cli.close()
574582
self.cli = None
575583
ThreadableTest.clientTearDown(self)
576584

@@ -4786,6 +4794,120 @@ def _testSend(self):
47864794
self.assertEqual(msg, MSG)
47874795

47884796

4797+
class PurePythonSocketPairTest(SocketPairTest):
4798+
4799+
# Explicitly use socketpair AF_INET or AF_INET6 to ensure that is the
4800+
# code path we're using regardless platform is the pure python one where
4801+
# `_socket.socketpair` does not exist. (AF_INET does not work with
4802+
# _socket.socketpair on many platforms).
4803+
def socketpair(self):
4804+
# called by super().setUp().
4805+
try:
4806+
return socket.socketpair(socket.AF_INET6)
4807+
except OSError:
4808+
return socket.socketpair(socket.AF_INET)
4809+
4810+
# Local imports in this class make for easy security fix backporting.
4811+
4812+
def setUp(self):
4813+
import _socket
4814+
self._orig_sp = getattr(_socket, 'socketpair', None)
4815+
if self._orig_sp is not None:
4816+
# This forces the version using the non-OS provided socketpair
4817+
# emulation via an AF_INET socket in Lib/socket.py.
4818+
del _socket.socketpair
4819+
import importlib
4820+
global socket
4821+
socket = importlib.reload(socket)
4822+
else:
4823+
pass # This platform already uses the non-OS provided version.
4824+
super().setUp()
4825+
4826+
def tearDown(self):
4827+
super().tearDown()
4828+
import _socket
4829+
if self._orig_sp is not None:
4830+
# Restore the default socket.socketpair definition.
4831+
_socket.socketpair = self._orig_sp
4832+
import importlib
4833+
global socket
4834+
socket = importlib.reload(socket)
4835+
4836+
def test_recv(self):
4837+
msg = self.serv.recv(1024)
4838+
self.assertEqual(msg, MSG)
4839+
4840+
def _test_recv(self):
4841+
self.cli.send(MSG)
4842+
4843+
def test_send(self):
4844+
self.serv.send(MSG)
4845+
4846+
def _test_send(self):
4847+
msg = self.cli.recv(1024)
4848+
self.assertEqual(msg, MSG)
4849+
4850+
def test_ipv4(self):
4851+
cli, srv = socket.socketpair(socket.AF_INET)
4852+
cli.close()
4853+
srv.close()
4854+
4855+
def _test_ipv4(self):
4856+
pass
4857+
4858+
@unittest.skipIf(not hasattr(_socket, 'IPPROTO_IPV6') or
4859+
not hasattr(_socket, 'IPV6_V6ONLY'),
4860+
"IPV6_V6ONLY option not supported")
4861+
@unittest.skipUnless(socket_helper.IPV6_ENABLED, 'IPv6 required for this test')
4862+
def test_ipv6(self):
4863+
cli, srv = socket.socketpair(socket.AF_INET6)
4864+
cli.close()
4865+
srv.close()
4866+
4867+
def _test_ipv6(self):
4868+
pass
4869+
4870+
def test_injected_authentication_failure(self):
4871+
orig_getsockname = socket.socket.getsockname
4872+
inject_sock = None
4873+
4874+
def inject_getsocketname(self):
4875+
nonlocal inject_sock
4876+
sockname = orig_getsockname(self)
4877+
# Connect to the listening socket ahead of the
4878+
# client socket.
4879+
if inject_sock is None:
4880+
inject_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
4881+
inject_sock.setblocking(False)
4882+
try:
4883+
inject_sock.connect(sockname[:2])
4884+
except (BlockingIOError, InterruptedError):
4885+
pass
4886+
inject_sock.setblocking(True)
4887+
return sockname
4888+
4889+
sock1 = sock2 = None
4890+
try:
4891+
socket.socket.getsockname = inject_getsocketname
4892+
with self.assertRaises(OSError):
4893+
sock1, sock2 = socket.socketpair()
4894+
finally:
4895+
socket.socket.getsockname = orig_getsockname
4896+
if inject_sock:
4897+
inject_sock.close()
4898+
if sock1: # This cleanup isn't needed on a successful test.
4899+
sock1.close()
4900+
if sock2:
4901+
sock2.close()
4902+
4903+
def _test_injected_authentication_failure(self):
4904+
# No-op. Exists for base class threading infrastructure to call.
4905+
# We could refactor this test into its own lesser class along with the
4906+
# setUp and tearDown code to construct an ideal; it is simpler to keep
4907+
# it here and live with extra overhead one this _one_ failure test.
4908+
pass
4909+
4910+
47894911
class NonBlockingTCPTests(ThreadedTCPSocketTest):
47904912

47914913
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)