Skip to content

Commit 70e7c72

Browse files
Add denial of fetch headers (#58553)
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 <[email protected]>
1 parent f6490dd commit 70e7c72

File tree

3 files changed

+80
-2
lines changed

3 files changed

+80
-2
lines changed

python/ray/dashboard/http_server_head.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,10 @@ async def browsers_no_post_put_middleware(request, handler):
307307
if (
308308
# A best effort test for browser traffic. All common browsers
309309
# start with Mozilla at the time of writing.
310-
dashboard_optional_utils.is_browser_request(request)
310+
(
311+
dashboard_optional_utils.is_browser_request(request)
312+
or dashboard_optional_utils.has_sec_fetch_headers(request)
313+
)
311314
and request.method in [hdrs.METH_POST, hdrs.METH_PUT]
312315
):
313316
return aiohttp.web.Response(

python/ray/dashboard/optional_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,30 @@ def is_browser_request(req: Request) -> bool:
138138
return req.headers["User-Agent"].startswith("Mozilla")
139139

140140

141+
def has_sec_fetch_headers(req: Request) -> bool:
142+
"""Checks for the existance of any of the sec-fetch-* headers"""
143+
return any(
144+
h in req.headers
145+
for h in (
146+
"Sec-Fetch-Mode",
147+
"Sec-Fetch-Dest",
148+
"Sec-Fetch-Site",
149+
"Sec-Fetch-User",
150+
)
151+
)
152+
153+
141154
def deny_browser_requests() -> Callable:
142155
"""Reject any requests that appear to be made by a browser"""
143156

144157
def decorator_factory(f: Callable) -> Callable:
145158
@functools.wraps(f)
146159
async def decorator(self, req: Request):
160+
if has_sec_fetch_headers(req):
161+
return Response(
162+
text="Browser requests not allowed",
163+
status=aiohttp.web.HTTPMethodNotAllowed.status_code,
164+
)
147165
if is_browser_request(req):
148166
return Response(
149167
text="Browser requests not allowed",

python/ray/dashboard/tests/test_dashboard.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,16 @@ def test_browser_no_post_no_put(enable_test_module, ray_start_with_dashboard):
419419
webui_url = ray_start_with_dashboard["webui_url"]
420420
webui_url = format_web_url(webui_url)
421421

422+
def dashboard_available():
423+
try:
424+
return requests.get(webui_url).status_code == 200
425+
except Exception:
426+
return False
427+
422428
timeout_seconds = 30
423429
start_time = time.time()
430+
wait_for_condition(dashboard_available)
424431
while True:
425-
time.sleep(3)
426432
try:
427433
# Starting and getting jobs should be fine from API clients
428434
response = requests.post(
@@ -458,6 +464,57 @@ def test_browser_no_post_no_put(enable_test_module, ray_start_with_dashboard):
458464
raise Exception("Timed out while testing.")
459465

460466

467+
@pytest.mark.skipif(
468+
os.environ.get("RAY_MINIMAL") == "1",
469+
reason="This test is not supposed to work for minimal installation.",
470+
)
471+
def test_deny_fetch_requests(enable_test_module, ray_start_with_dashboard):
472+
assert wait_until_server_available(ray_start_with_dashboard["webui_url"]) is True
473+
webui_url = ray_start_with_dashboard["webui_url"]
474+
webui_url = format_web_url(webui_url)
475+
476+
def dashboard_available():
477+
try:
478+
return requests.get(webui_url).status_code == 200
479+
except Exception:
480+
return False
481+
482+
timeout_seconds = 30
483+
start_time = time.time()
484+
wait_for_condition(dashboard_available)
485+
while True:
486+
try:
487+
# Starting and getting jobs should be fine from API clients
488+
response = requests.post(
489+
webui_url + "/api/jobs/", json={"entrypoint": "ls"}
490+
)
491+
response.raise_for_status()
492+
response = requests.get(webui_url + "/api/jobs/")
493+
response.raise_for_status()
494+
495+
# Starting job should be blocked for browsers
496+
response = requests.post(
497+
webui_url + "/api/jobs/",
498+
json={"entrypoint": "ls"},
499+
headers={
500+
"User-Agent": ("Spurious User Agent"),
501+
"Sec-Fetch-Site": ("cross-site"),
502+
},
503+
)
504+
with pytest.raises(HTTPError):
505+
response.raise_for_status()
506+
507+
# Getting jobs should be fine for browsers
508+
response = requests.get(webui_url + "/api/jobs/")
509+
response.raise_for_status()
510+
break
511+
except (AssertionError, requests.exceptions.ConnectionError) as e:
512+
logger.info("Retry because of %s", e)
513+
finally:
514+
if time.time() > start_time + timeout_seconds:
515+
raise Exception("Timed out while testing.")
516+
517+
461518
@pytest.mark.skipif(
462519
os.environ.get("RAY_MINIMAL") == "1",
463520
reason="This test is not supposed to work for minimal installation.",

0 commit comments

Comments
 (0)