Skip to content

Commit 2621a8a

Browse files
miss-islingtonsethmlarsongpsheadambv
authored
[3.8] gh-122133: Authenticate socket connection for socket.socketpair() fallback (GH-122134) (GH-122429)
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]> Co-authored-by: Łukasz Langa <[email protected]>
1 parent 1fb69b3 commit 2621a8a

File tree

3 files changed

+147
-3
lines changed

3 files changed

+147
-3
lines changed

Lib/socket.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,23 @@ def socketpair(family=AF_INET, type=SOCK_STREAM, proto=0):
611611
raise
612612
finally:
613613
lsock.close()
614+
615+
# Authenticating avoids using a connection from something else
616+
# able to connect to {host}:{port} instead of us.
617+
# We expect only AF_INET and AF_INET6 families.
618+
try:
619+
if (
620+
ssock.getsockname() != csock.getpeername()
621+
or csock.getsockname() != ssock.getpeername()
622+
):
623+
raise ConnectionError("Unexpected peer connection")
624+
except:
625+
# getsockname() and getpeername() can fail
626+
# if either socket isn't connected.
627+
ssock.close()
628+
csock.close()
629+
raise
630+
614631
return (ssock, csock)
615632
__all__.append("socketpair")
616633

Lib/test/test_socket.py

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -508,19 +508,27 @@ class SocketPairTest(unittest.TestCase, ThreadableTest):
508508
def __init__(self, methodName='runTest'):
509509
unittest.TestCase.__init__(self, methodName=methodName)
510510
ThreadableTest.__init__(self)
511+
self.cli = None
512+
self.serv = None
513+
514+
def socketpair(self):
515+
# To be overridden by some child classes.
516+
return socket.socketpair()
511517

512518
def setUp(self):
513-
self.serv, self.cli = socket.socketpair()
519+
self.serv, self.cli = self.socketpair()
514520

515521
def tearDown(self):
516-
self.serv.close()
522+
if self.serv:
523+
self.serv.close()
517524
self.serv = None
518525

519526
def clientSetUp(self):
520527
pass
521528

522529
def clientTearDown(self):
523-
self.cli.close()
530+
if self.cli:
531+
self.cli.close()
524532
self.cli = None
525533
ThreadableTest.clientTearDown(self)
526534

@@ -4307,6 +4315,120 @@ def _testSend(self):
43074315
self.assertEqual(msg, MSG)
43084316

43094317

4318+
class PurePythonSocketPairTest(SocketPairTest):
4319+
4320+
# Explicitly use socketpair AF_INET or AF_INET6 to ensure that is the
4321+
# code path we're using regardless platform is the pure python one where
4322+
# `_socket.socketpair` does not exist. (AF_INET does not work with
4323+
# _socket.socketpair on many platforms).
4324+
def socketpair(self):
4325+
# called by super().setUp().
4326+
try:
4327+
return socket.socketpair(socket.AF_INET6)
4328+
except OSError:
4329+
return socket.socketpair(socket.AF_INET)
4330+
4331+
# Local imports in this class make for easy security fix backporting.
4332+
4333+
def setUp(self):
4334+
import _socket
4335+
self._orig_sp = getattr(_socket, 'socketpair', None)
4336+
if self._orig_sp is not None:
4337+
# This forces the version using the non-OS provided socketpair
4338+
# emulation via an AF_INET socket in Lib/socket.py.
4339+
del _socket.socketpair
4340+
import importlib
4341+
global socket
4342+
socket = importlib.reload(socket)
4343+
else:
4344+
pass # This platform already uses the non-OS provided version.
4345+
super().setUp()
4346+
4347+
def tearDown(self):
4348+
super().tearDown()
4349+
import _socket
4350+
if self._orig_sp is not None:
4351+
# Restore the default socket.socketpair definition.
4352+
_socket.socketpair = self._orig_sp
4353+
import importlib
4354+
global socket
4355+
socket = importlib.reload(socket)
4356+
4357+
def test_recv(self):
4358+
msg = self.serv.recv(1024)
4359+
self.assertEqual(msg, MSG)
4360+
4361+
def _test_recv(self):
4362+
self.cli.send(MSG)
4363+
4364+
def test_send(self):
4365+
self.serv.send(MSG)
4366+
4367+
def _test_send(self):
4368+
msg = self.cli.recv(1024)
4369+
self.assertEqual(msg, MSG)
4370+
4371+
def test_ipv4(self):
4372+
cli, srv = socket.socketpair(socket.AF_INET)
4373+
cli.close()
4374+
srv.close()
4375+
4376+
def _test_ipv4(self):
4377+
pass
4378+
4379+
@unittest.skipIf(not hasattr(_socket, 'IPPROTO_IPV6') or
4380+
not hasattr(_socket, 'IPV6_V6ONLY'),
4381+
"IPV6_V6ONLY option not supported")
4382+
@unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test')
4383+
def test_ipv6(self):
4384+
cli, srv = socket.socketpair(socket.AF_INET6)
4385+
cli.close()
4386+
srv.close()
4387+
4388+
def _test_ipv6(self):
4389+
pass
4390+
4391+
def test_injected_authentication_failure(self):
4392+
orig_getsockname = socket.socket.getsockname
4393+
inject_sock = None
4394+
4395+
def inject_getsocketname(self):
4396+
nonlocal inject_sock
4397+
sockname = orig_getsockname(self)
4398+
# Connect to the listening socket ahead of the
4399+
# client socket.
4400+
if inject_sock is None:
4401+
inject_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
4402+
inject_sock.setblocking(False)
4403+
try:
4404+
inject_sock.connect(sockname[:2])
4405+
except (BlockingIOError, InterruptedError):
4406+
pass
4407+
inject_sock.setblocking(True)
4408+
return sockname
4409+
4410+
sock1 = sock2 = None
4411+
try:
4412+
socket.socket.getsockname = inject_getsocketname
4413+
with self.assertRaises(OSError):
4414+
sock1, sock2 = socket.socketpair()
4415+
finally:
4416+
socket.socket.getsockname = orig_getsockname
4417+
if inject_sock:
4418+
inject_sock.close()
4419+
if sock1: # This cleanup isn't needed on a successful test.
4420+
sock1.close()
4421+
if sock2:
4422+
sock2.close()
4423+
4424+
def _test_injected_authentication_failure(self):
4425+
# No-op. Exists for base class threading infrastructure to call.
4426+
# We could refactor this test into its own lesser class along with the
4427+
# setUp and tearDown code to construct an ideal; it is simpler to keep
4428+
# it here and live with extra overhead one this _one_ failure test.
4429+
pass
4430+
4431+
43104432
class NonBlockingTCPTests(ThreadedTCPSocketTest):
43114433

43124434
def __init__(self, methodName='runTest'):
Lines changed: 5 additions & 0 deletions
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)