CVE-2024-1729: Time-of-check Time-of-use (TOCTOU) Race Condition (CWE-367)
value: A timing attack vulnerability exists in the gradio-app/gradio repository, specifically within the login function in routes.py. The vulnerability arises from the use of a direct comparison operation (`app.auth[username] == password`) to validate user credentials, which can be exploited to guess passwords based on response times. Successful exploitation of this vulnerability could allow an attacker to bypass authentication mechanisms and gain unauthorized access.
CWE description: The product checks the state of a resource before using that resource, but the resource's state can change between the check and the use in a way that invalidates the results of the check. This can cause the product to perform invalid actions when the resource is in an unexpected state.
Published: 2024-03-29T05:15Z
Repository: https://github.com/gradio-app/gradio
Language: Python
Files changed: .changeset/fine-pets-switch.md, gradio/route_utils.py, gradio/routes.py, test/test_routes.py
Commit date: 2024-02-15 12:59:08 -0800
Commit message: Prevent timing attacks to guess Gradio passwords (#7440)

* secure timing

* add changeset

* format

* add changeset

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Diff: diff --git a/.changeset/fine-pets-switch.md b/.changeset/fine-pets-switch.md
new file mode 100644
index 000000000..2898ae86b
--- /dev/null
+++ b/.changeset/fine-pets-switch.md
@@ -0,0 +1,5 @@
+---
+"gradio": patch
+---
+
+feat:Prevent timing attacks to guess Gradio passwords
diff --git a/gradio/route_utils.py b/gradio/route_utils.py
index 097bd26c4..7bc14cfa9 100644
--- a/gradio/route_utils.py
+++ b/gradio/route_utils.py
@@ -1,8 +1,9 @@
 from __future__ import annotations
 
 import hashlib
+import hmac
 import json
 import shutil
 from collections import deque
 from dataclasses import dataclass as python_dataclass
 from tempfile import NamedTemporaryFile, _TemporaryFileWrapper
@@ -567,10 +568,14 @@ def update_root_in_config(config: dict, root: str) -> dict:
     """
     Updates the root "key" in the config dictionary to the new root url. If the
     root url has changed, all of the urls in the config that correspond to component
     file urls are updated to use the new root url.
     """
-    previous_root = config.get("root", None)
+    previous_root = config.get("root")
     if previous_root is None or previous_root != root:
         config["root"] = root
         config = processing_utils.add_root_url(config, root, previous_root)
     return config
+
+
+def compare_passwords_securely(input_password: str, correct_password: str) -> bool:
+    return hmac.compare_digest(input_password.encode(), correct_password.encode())
diff --git a/gradio/routes.py b/gradio/routes.py
index ef8d2eabe..077c51332 100644
--- a/gradio/routes.py
+++ b/gradio/routes.py
@@ -61,10 +61,11 @@ from gradio.route_utils import (  # noqa: F401
     FileUploadProgressNotTrackedError,
     GradioMultiPartParser,
     GradioUploadFile,
     MultiPartException,
     Request,
+    compare_passwords_securely,
     move_uploaded_files_to_cache,
 )
 from gradio.state_holder import StateHolder
 from gradio.utils import (
     get_package_version,
@@ -269,11 +270,11 @@ class App(FastAPI):
             if app.auth is None:
                 return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
             if (
                 not callable(app.auth)
                 and username in app.auth
-                and app.auth[username] == password
+                and compare_passwords_securely(password, app.auth[username])  # type: ignore
             ) or (callable(app.auth) and app.auth.__call__(username, password)):
                 token = secrets.token_urlsafe(16)
                 app.tokens[token] = username
                 response = JSONResponse(content={"success": True})
                 response.set_cookie(
diff --git a/test/test_routes.py b/test/test_routes.py
index f54da98c6..e7c3ae1ae 100644
--- a/test/test_routes.py
+++ b/test/test_routes.py
@@ -23,11 +23,15 @@ from gradio import (
     Textbox,
     close_all,
     routes,
     wasm_utils,
 )
-from gradio.route_utils import FnIndexInferError, get_root_url
+from gradio.route_utils import (
+    FnIndexInferError,
+    compare_passwords_securely,
+    get_root_url,
+)
 
 
 @pytest.fixture()
 def test_client():
     io = Interface(lambda x: x + x, "text", "text")
@@ -919,5 +923,13 @@ def test_component_server_endpoints(connect):
     ],
 )
 def test_get_root_url(request_url, route_path, root_path, expected_root_url):
     request = Request({"path": request_url, "type": "http", "headers": {}})
     assert get_root_url(request, route_path, root_path) == expected_root_url
+
+
+def test_compare_passwords_securely():
+    password1 = "password"
+    password2 = "pässword"
+    assert compare_passwords_securely(password1, password1)
+    assert not compare_passwords_securely(password1, password2)
+    assert compare_passwords_securely(password2, password2)

Vulnerable code:     previous_root = config.get("root", None)
                and app.auth[username] == password
from gradio.route_utils import FnIndexInferError, get_root_url
Fixed code: ---
"gradio": patch
---

feat:Prevent timing attacks to guess Gradio passwords
import hmac
    previous_root = config.get("root")


def compare_passwords_securely(input_password: str, correct_password: str) -> bool:
    return hmac.compare_digest(input_password.encode(), correct_password.encode())
    compare_passwords_securely,
                and compare_passwords_securely(password, app.auth[username])  # type: ignore
from gradio.route_utils import (
    FnIndexInferError,
    compare_passwords_securely,
    get_root_url,
)


def test_compare_passwords_securely():
    password1 = "password"
    password2 = "pässword"
    assert compare_passwords_securely(password1, password1)
    assert not compare_passwords_securely(password1, password2)
    assert compare_passwords_securely(password2, password2)