CVE-2024-1727 (Python, published 2024-03-21T20:15Z)
Description: value: A Cross-Site Request Forgery (CSRF) vulnerability in gradio-app/gradio allows attackers to upload multiple large files to a victim's system if they are running Gradio locally. By crafting a malicious HTML page that triggers an unauthorized file upload to the victim's server, an attacker can deplete the system's disk space, potentially leading to a denial of service. This issue affects the file upload functionality as implemented in gradio/routes.py.
Severity: nan (CVSS v3 base score: )
Weakness: CWE-352 \u2014 Cross-Site Request Forgery (CSRF)
The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
Repository: https://github.com/gradio-app/gradio
Files changed:
.changeset/olive-symbols-heal.md
gradio/route_utils.py
gradio/routes.py
test/test_routes.py
Commit message: Tighten CORS rules (#7503)

* tighten cors rules

* add changeset

* cors policy

* cors

* add changeset

* lint

* changes

* changes

* changes

* logging

* add null

* changes

* changes

* options

* options

* safe changes

* let browser enforce cors

* clean

* route utils

* fix

* fix test

* fix

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>

Vulnerable code:
from fastapi.middleware.cors import CORSMiddleware
            app.add_middleware(
                CORSMiddleware,
                allow_origins=["*"],
                allow_methods=["*"],
                allow_headers=["*"],
            )

Fixed code:
---
"gradio": patch
---

feat:Tighten CORS rules
from urllib.parse import urlparse
from starlette.middleware.base import BaseHTTPMiddleware


def get_hostname(url: str) -> str:
    """
    Returns the hostname of a given url, or an empty string if the url cannot be parsed.
    Examples:
        get_hostname("https://www.gradio.app") -> "www.gradio.app"
        get_hostname("localhost:7860") -> "localhost"
        get_hostname("127.0.0.1") -> "127.0.0.1"
    """
    if not url:
        return ""
    if "://" not in url:
        url = "http://" + url
    try:
        return urlparse(url).hostname or ""
    except Exception:
        return ""


class CustomCORSMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: fastapi.Request, call_next):
        host: str = request.headers.get("host", "")
        origin: str = request.headers.get("origin", "")
        host_name = get_hostname(host)
        origin_name = get_hostname(origin)

        # Any of these hosts suggests that the Gradio app is running locally.
        # Note: "null" is a special case that happens if a Gradio app is running
        # as an embedded web component in a local static webpage.
        localhost_aliases = ["localhost", "127.0.0.1", "0.0.0.0", "null"]
        is_preflight = (
            request.method == "OPTIONS"
            and "access-control-request-method" in request.headers
        )

        if host_name in localhost_aliases and origin_name not in localhost_aliases:
            allow_origin_header = None
        else:
            allow_origin_header = origin

        if is_preflight:
            response = fastapi.Response()
        else:
            response = await call_next(request)

        if allow_origin_header:
            response.headers["Access-Control-Allow-Origin"] = allow_origin_header
        response.headers[
            "Access-Control-Allow-Methods"
        ] = "GET, POST, PUT, DELETE, OPTIONS"
        response.headers[
            "Access-Control-Allow-Headers"
        ] = "Origin, Content-Type, Accept"
        return response
    CustomCORSMiddleware,
            app.add_middleware(CustomCORSMiddleware)
    def test_cors_restrictions(self):
        io = gr.Interface(lambda s: s.name, gr.File(), gr.File())
        app, _, _ = io.launch(prevent_thread_lock=True)
        client = TestClient(app)
        custom_headers = {
            "host": "localhost:7860",
            "origin": "https://example.com",
        }
        file_response = client.get("/config", headers=custom_headers)
        assert "access-control-allow-origin" not in file_response.headers
        custom_headers = {
            "host": "localhost:7860",
            "origin": "127.0.0.1",
        }
        file_response = client.get("/config", headers=custom_headers)
        assert file_response.headers["access-control-allow-origin"] == "127.0.0.1"
        io.close()