Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 220e31a

Browse filesBrowse 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 <seth@python.org> Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent bad8497 commit 220e31a
Copy full SHA for 220e31a

File tree

3 files changed

+147
-3
lines changed
Filter options

3 files changed

+147
-3
lines changed

‎Lib/socket.py

Copy file name to clipboardExpand all lines: Lib/socket.py
+17Lines changed: 17 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: Lib/test/test_socket.py
+125-3Lines changed: 125 additions & 3 deletions
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'):
+5Lines 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 <greg@krypto.org> and Seth Larson <seth@python.org>. Reported by Ellie
5+
<el@horse64.org>

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.