Skip to content

Add denial of fetch headers#58553

Merged
edoakes merged 7 commits into
masterfrom
richo/sec-fetch-headers
Nov 14, 2025
Merged

Add denial of fetch headers#58553
edoakes merged 7 commits into
masterfrom
richo/sec-fetch-headers

Conversation

@richo-anyscale

@richo-anyscale richo-anyscale commented Nov 11, 2025

Copy link
Copy Markdown
Contributor

This causes the dashboard to be more thorough in it's attempts to deny browsers access to the job creation APIs

@richo-anyscale richo-anyscale requested a review from a team as a code owner November 11, 2025 22:05

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances security by adding a check for Sec-Fetch-* headers to better identify and block API requests originating from browsers. This is a solid improvement. My review includes suggestions to make the implementation more concise and robust, such as using more idiomatic Python, correcting the HTTP status code for denied requests, and parameterizing the new test to reduce code duplication and improve maintainability.

Comment thread python/ray/dashboard/optional_utils.py Outdated
Comment on lines +140 to +147
def has_sec_fetch_headers(req: Request) -> bool:
"""Checks for the existance of any of the sec-fetch-* headers.

"""
for header in ("Sec-Fetch-Mode", "Sec-Fetch-Dest", "Sec-Fetch-Site", "Sec-Fetch-User"):
if header in req.headers:
return True
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The implementation of has_sec_fetch_headers can be made more concise and Pythonic by using the any() built-in function. This improves readability. Also, there's a typo ('existance') in the docstring, which should be 'existence', and the empty line can be removed for better formatting.

def has_sec_fetch_headers(req: Request) -> bool:
    """Checks for the existence of any of the sec-fetch-* headers."""
    return any(h in req.headers for h in ("Sec-Fetch-Mode", "Sec-Fetch-Dest", "Sec-Fetch-Site", "Sec-Fetch-User"))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest the same thing -- well done, AI overlord!

Comment on lines +460 to +502
def test_deny_fetch_requests(enable_test_module, ray_start_with_dashboard):
assert wait_until_server_available(ray_start_with_dashboard["webui_url"]) is True
webui_url = ray_start_with_dashboard["webui_url"]
webui_url = format_web_url(webui_url)

timeout_seconds = 30
start_time = time.time()
while True:
time.sleep(3)
try:
# Starting and getting jobs should be fine from API clients
response = requests.post(
webui_url + "/api/jobs/", json={"entrypoint": "ls"}
)
response.raise_for_status()
response = requests.get(webui_url + "/api/jobs/")
response.raise_for_status()

# Starting job should be blocked for browsers
response = requests.post(
webui_url + "/api/jobs/",
json={"entrypoint": "ls"},
headers={
"User-Agent": (
"Spurious User Agent"
),
"Sec-Fetch-Site": (
"cross-site"
)
},
)
with pytest.raises(HTTPError):
response.raise_for_status()

# Getting jobs should be fine for browsers
response = requests.get(webui_url + "/api/jobs/")
response.raise_for_status()
break
except (AssertionError, requests.exceptions.ConnectionError) as e:
logger.info("Retry because of %s", e)
finally:
if time.time() > start_time + timeout_seconds:
raise Exception("Timed out while testing.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The new test test_deny_fetch_requests is very similar in structure to the existing test_browser_no_post_no_put test. To improve maintainability and avoid code duplication, you could parameterize this test to cover both scenarios (blocking by User-Agent and by Sec-Fetch-* headers). This would allow you to consolidate the logic and eventually remove the test_browser_no_post_no_put test.

@pytest.mark.parametrize(
    "headers_to_block",
    [
        {
            "User-Agent": (
                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/119.0.0.0 Safari/537.36"
            )
        },
        {"User-Agent": "Spurious User Agent", "Sec-Fetch-Site": "cross-site"},
    ],
)
def test_deny_browser_like_requests(
    headers_to_block, enable_test_module, ray_start_with_dashboard
):
    assert wait_until_server_available(ray_start_with_dashboard["webui_url"]) is True
    webui_url = ray_start_with_dashboard["webui_url"]
    webui_url = format_web_url(webui_url)

    timeout_seconds = 30
    start_time = time.time()
    while True:
        time.sleep(3)
        try:
            # Starting and getting jobs should be fine from API clients
            response = requests.post(
                webui_url + "/api/jobs/", json={"entrypoint": "ls"}
            )
            response.raise_for_status()
            response = requests.get(webui_url + "/api/jobs/")
            response.raise_for_status()

            # Starting job should be blocked for browsers
            response = requests.post(
                webui_url + "/api/jobs/",
                json={"entrypoint": "ls"},
                headers=headers_to_block,
            )
            with pytest.raises(HTTPError):
                response.raise_for_status()

            # Getting jobs should be fine for browsers
            response = requests.get(webui_url + "/api/jobs/")
            response.raise_for_status()
            break
        except (AssertionError, requests.exceptions.ConnectionError) as e:
            logger.info("Retry because of %s", e)
        finally:
            if time.time() > start_time + timeout_seconds:
                raise Exception("Timed out while testing.")
Comment thread python/ray/dashboard/optional_utils.py Outdated
Comment on lines +140 to +147
def has_sec_fetch_headers(req: Request) -> bool:
"""Checks for the existance of any of the sec-fetch-* headers.

"""
for header in ("Sec-Fetch-Mode", "Sec-Fetch-Dest", "Sec-Fetch-Site", "Sec-Fetch-User"):
if header in req.headers:
return True
return False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest the same thing -- well done, AI overlord!

Comment on lines +465 to +468
timeout_seconds = 30
start_time = time.time()
while True:
time.sleep(3)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the timeout & sleep needed for?

if we need to keep it, can refactor to use the wait_for_condition utility that is used in many other tests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just cargo culted the other test, I can try to refactor them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, so I did this refactor, but now I'm looking closer at it, I'm betting it's related to whatever we felt the retry logic was needed for (This is before I was around). I moved it out of the loop and we'll see if it becomes flaky.

@ray-gardener ray-gardener Bot added the core Issues that should be addressed in Ray Core label Nov 12, 2025
@edoakes edoakes added the go add ONLY when ready to merge, run all tests label Nov 12, 2025
@edoakes

edoakes commented Nov 12, 2025

Copy link
Copy Markdown
Collaborator

Looks like test is failing in CI: https://buildkite.com/ray-project/microcheck/builds/30837#019a7537-db13-4b75-97c6-0e20cc0c5969/632-2692

Please also sign off the commits to fix DCO build

@richo-anyscale richo-anyscale force-pushed the richo/sec-fetch-headers branch from f69ba0e to 7559f49 Compare November 13, 2025 19:50
Comment thread python/ray/dashboard/tests/test_dashboard.py
@richo-anyscale richo-anyscale force-pushed the richo/sec-fetch-headers branch 2 times, most recently from 1870cfb to 940eb7c Compare November 13, 2025 20:07
Comment thread python/ray/dashboard/tests/test_dashboard.py
@richo-anyscale richo-anyscale force-pushed the richo/sec-fetch-headers branch from 940eb7c to abdd6f1 Compare November 13, 2025 20:11
Comment thread python/ray/dashboard/tests/test_dashboard.py Outdated
@richo-anyscale richo-anyscale force-pushed the richo/sec-fetch-headers branch from 2fd984b to 03c1a2f Compare November 13, 2025 20:45
Comment thread python/ray/dashboard/tests/test_dashboard.py Outdated
Comment thread python/ray/dashboard/tests/test_dashboard.py Outdated
@richo-anyscale richo-anyscale force-pushed the richo/sec-fetch-headers branch from 03c1a2f to 9476253 Compare November 13, 2025 20:48
timeout_seconds = 30
start_time = time.time()
while True:
time.sleep(3)
wait_for_condition(dashboard_available)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Uncaught Timeout Error Prevents Proper Retries

The wait_for_condition call inside the retry loop raises RuntimeError on timeout, which isn't caught by the except clause that only handles AssertionError and ConnectionError. This causes the test to fail after 10 seconds even though the outer timeout is 30 seconds, preventing proper retry behavior when the dashboard is slow to start.

Fix in Cursor Fix in Web

Comment thread python/ray/dashboard/tests/test_dashboard.py Outdated
@richo-anyscale richo-anyscale force-pushed the richo/sec-fetch-headers branch 3 times, most recently from 2336f38 to 7c31868 Compare November 13, 2025 20:57
Signed-off-by: Richo Healey <richo@anyscale.com>
Signed-off-by: Richo Healey <richo@anyscale.com>
Signed-off-by: Richo Healey <richo@anyscale.com>
Signed-off-by: Richo Healey <richo@anyscale.com>
@richo-anyscale richo-anyscale force-pushed the richo/sec-fetch-headers branch from 7c31868 to 65e4ce4 Compare November 13, 2025 23:14
Signed-off-by: Richo Healey <richo@anyscale.com>
@richo-anyscale richo-anyscale force-pushed the richo/sec-fetch-headers branch from 16e0186 to 28e0a43 Compare November 14, 2025 00:02
Comment thread python/ray/dashboard/tests/test_dashboard.py Outdated
Comment thread python/ray/dashboard/optional_utils.py Outdated
Comment thread python/ray/dashboard/optional_utils.py
Signed-off-by: Richo Healey <richo@anyscale.com>
@richo-anyscale richo-anyscale force-pushed the richo/sec-fetch-headers branch from fc385ce to f44ce4e Compare November 14, 2025 06:24
Signed-off-by: Richo Healey <richo@anyscale.com>
Comment thread python/ray/dashboard/tests/test_dashboard.py
@edoakes edoakes merged commit 70e7c72 into master Nov 14, 2025
6 checks passed
@edoakes edoakes deleted the richo/sec-fetch-headers branch November 14, 2025 18:21
@richo-anyscale

Copy link
Copy Markdown
Contributor Author

🙌

edoakes pushed a commit to edoakes/ray that referenced this pull request Nov 27, 2025
This causes the dashboard to be more thorough in it's attempts to deny
browsers access to the job creation APIs

---------

Signed-off-by: Richo Healey <richo@anyscale.com>
Signed-off-by: Edward Oakes <ed.nmi.oakes@gmail.com>
aslonnie added a commit that referenced this pull request Nov 28, 2025
Cherry pick #58553 #58648 #59042

---------

Signed-off-by: Richo Healey <richo@anyscale.com>
Signed-off-by: Edward Oakes <ed.nmi.oakes@gmail.com>
Co-authored-by: richo-anyscale <richo@anyscale.com>
Co-authored-by: Lonnie Liu <95255098+aslonnie@users.noreply.github.com>
SheldonTsen pushed a commit to SheldonTsen/ray that referenced this pull request Dec 1, 2025
This causes the dashboard to be more thorough in it's attempts to deny
browsers access to the job creation APIs

---------

Signed-off-by: Richo Healey <richo@anyscale.com>
Future-Outlier pushed a commit to Future-Outlier/ray that referenced this pull request Dec 7, 2025
This causes the dashboard to be more thorough in it's attempts to deny
browsers access to the job creation APIs

---------

Signed-off-by: Richo Healey <richo@anyscale.com>
Signed-off-by: Future-Outlier <eric901201@gmail.com>
weiquanlee pushed a commit to antgroup/ant-ray that referenced this pull request Dec 11, 2025
Cherry pick ray-project#58553 ray-project#58648 ray-project#59042

---------

Signed-off-by: Richo Healey <richo@anyscale.com>
Signed-off-by: Edward Oakes <ed.nmi.oakes@gmail.com>
Co-authored-by: richo-anyscale <richo@anyscale.com>
Co-authored-by: Lonnie Liu <95255098+aslonnie@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Issues that should be addressed in Ray Core go add ONLY when ready to merge, run all tests

2 participants