Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion python/ray/dashboard/http_server_head.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,10 @@ async def browsers_no_post_put_middleware(request, handler):
if (
# A best effort test for browser traffic. All common browsers
# start with Mozilla at the time of writing.
dashboard_optional_utils.is_browser_request(request)
(
dashboard_optional_utils.is_browser_request(request)
or dashboard_optional_utils.has_sec_fetch_headers(request)
)
and request.method in [hdrs.METH_POST, hdrs.METH_PUT]
):
return aiohttp.web.Response(
Expand Down
18 changes: 18 additions & 0 deletions python/ray/dashboard/optional_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,30 @@ def is_browser_request(req: Request) -> bool:
return req.headers["User-Agent"].startswith("Mozilla")


def has_sec_fetch_headers(req: Request) -> bool:
"""Checks for the existance 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",
)
)


def deny_browser_requests() -> Callable:
"""Reject any requests that appear to be made by a browser"""

def decorator_factory(f: Callable) -> Callable:
@functools.wraps(f)
async def decorator(self, req: Request):
if has_sec_fetch_headers(req):
return Response(
text="Browser requests not allowed",
status=aiohttp.web.HTTPMethodNotAllowed.status_code,
)
if is_browser_request(req):
return Response(
text="Browser requests not allowed",
Expand Down
59 changes: 58 additions & 1 deletion python/ray/dashboard/tests/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,10 +419,16 @@ def test_browser_no_post_no_put(enable_test_module, ray_start_with_dashboard):
webui_url = ray_start_with_dashboard["webui_url"]
webui_url = format_web_url(webui_url)

def dashboard_available():
try:
return requests.get(webui_url).status_code == 200
except Exception:
return False

timeout_seconds = 30
start_time = time.time()
wait_for_condition(dashboard_available)
while True:
time.sleep(3)
try:
# Starting and getting jobs should be fine from API clients
response = requests.post(
Expand Down Expand Up @@ -458,6 +464,57 @@ def test_browser_no_post_no_put(enable_test_module, ray_start_with_dashboard):
raise Exception("Timed out while testing.")


@pytest.mark.skipif(
os.environ.get("RAY_MINIMAL") == "1",
reason="This test is not supposed to work for minimal installation.",
)
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)

def dashboard_available():
try:
return requests.get(webui_url).status_code == 200
except Exception:
return False

timeout_seconds = 30
start_time = time.time()
wait_for_condition(dashboard_available)
while True:
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.")


@pytest.mark.skipif(
os.environ.get("RAY_MINIMAL") == "1",
reason="This test is not supposed to work for minimal installation.",
Expand Down