]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
auth: Correctly parse check_authentication response
authorBen Darnell <ben@bendarnell.com>
Thu, 21 May 2026 20:02:48 +0000 (16:02 -0400)
committerBen Darnell <ben@bendarnell.com>
Wed, 27 May 2026 16:07:30 +0000 (12:07 -0400)
This previously used substring search, which is incorrect, although
unlikely to be a vulnerability because there are no free-form text
fields allowed in this response format.

tornado/auth.py
tornado/test/auth_test.py

index 94a6c50c14a18b8f83150320603bc2fef69f9829..31115f6fbcc45335fc0cfcc805aa1030a4fdc9a5 100644 (file)
@@ -73,6 +73,7 @@ import base64
 import binascii
 import hashlib
 import hmac
+import re
 import time
 import urllib.parse
 import uuid
@@ -216,7 +217,7 @@ class OpenIdMixin:
         self, response: httpclient.HTTPResponse
     ) -> dict[str, Any]:
         handler = cast(RequestHandler, self)
-        if b"is_valid:true" not in response.body:
+        if re.search(rb"(?m)^is_valid:true$", response.body) is None:
             raise AuthError("Invalid OpenID response: %r" % response.body)
 
         # Make sure we got back at least an email from attribute exchange
index 28fa73094675165c8b0727fe4cfd3ad6212d607f..a411a159a3617e6021ee10bfa8f92028621e74b0 100644 (file)
@@ -41,10 +41,20 @@ class OpenIdClientLoginHandler(RequestHandler, OpenIdMixin):
 
 
 class OpenIdServerAuthenticateHandler(RequestHandler):
+    flip_flop = False
+
     def post(self):
         if self.get_argument("openid.mode") != "check_authentication":
             raise Exception("incorrect openid.mode %r")
-        self.write("is_valid:true")
+        # Cover both orderings of the response parameters if we call this handler twice.
+        # (the flip_flop side effect is simpler than plumbing parameters around).
+        # We check both orderings to catch mistaken uses of re.match instead of re.search
+        # or incorrect matching of the newline characters.
+        if type(self).flip_flop:
+            self.write("is_valid:true\nns:http://specs.openid.net/auth/2.0\n")
+        else:
+            self.write("ns:http://specs.openid.net/auth/2.0\nis_valid:true\n")
+        type(self).flip_flop = not type(self).flip_flop
 
 
 class OAuth1ClientLoginHandler(RequestHandler, OAuthMixin):
@@ -338,15 +348,17 @@ class AuthTest(AsyncHTTPTestCase):
         self.assertIn("/openid/server/authenticate?", response.headers["Location"])
 
     def test_openid_get_user(self):
-        response = self.fetch(
-            "/openid/client/login?openid.mode=blah"
-            "&openid.ns.ax=http://openid.net/srv/ax/1.0"
-            "&openid.ax.type.email=http://axschema.org/contact/email"
-            "&openid.ax.value.email=foo@example.com"
-        )
-        response.rethrow()
-        parsed = json_decode(response.body)
-        self.assertEqual(parsed["email"], "foo@example.com")
+        for i in range(2):
+            with self.subTest(i=i):
+                response = self.fetch(
+                    "/openid/client/login?openid.mode=blah"
+                    "&openid.ns.ax=http://openid.net/srv/ax/1.0"
+                    "&openid.ax.type.email=http://axschema.org/contact/email"
+                    "&openid.ax.value.email=foo@example.com"
+                )
+                response.rethrow()
+                parsed = json_decode(response.body)
+                self.assertEqual(parsed["email"], "foo@example.com")
 
     def test_oauth10_redirect(self):
         response = self.fetch("/oauth10/client/login", follow_redirects=False)