diff --git a/CHANGELOG.md b/CHANGELOG.md index 0257ac88..e05dc0eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ where X.Y.Z is the semver of the most recent choreographer release. ### Fixed - Build the `ChromeNotFoundError` message as one string, so it no longer prints as a tuple [[#314](https://github.com/plotly/choreographer/pull/314)], with thanks to @Blizzeq for the contribution! +- Escape the URL in the page-ready check, so a backtick no longer counts as a loaded page and a `${...}` sequence no longer runs as JavaScript [[#317](https://github.com/plotly/choreographer/pull/317)] ## [1.4.0] -- 2026-09-16 diff --git a/src/choreographer/protocol/devtools_async_helpers.py b/src/choreographer/protocol/devtools_async_helpers.py index c1d2d250..edb42d52 100644 --- a/src/choreographer/protocol/devtools_async_helpers.py +++ b/src/choreographer/protocol/devtools_async_helpers.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import json from typing import TYPE_CHECKING import logistro @@ -34,9 +35,9 @@ async def _check_document_ready(session: Session, url: str) -> BrowserResponse: new Promise((resolve) => { if ( (document.readyState === 'complete') && - (window.location==`""" # CONCATENATE! - f"{url!s}" - """`) + (window.location==""" # CONCATENATE! + f"{json.dumps(url)}" + """) ){ resolve("Was complete"); } else { diff --git a/tests/test_devtools_async_helpers.py b/tests/test_devtools_async_helpers.py index 86a83132..ecee1e7b 100644 --- a/tests/test_devtools_async_helpers.py +++ b/tests/test_devtools_async_helpers.py @@ -52,6 +52,23 @@ async def test_create_and_wait(browser): await create_and_wait(browser, url="http://192.0.2.1:9999", timeout=0.5) +@pytest.mark.asyncio +async def test_create_and_wait_escapes_url(browser): + """Test that create_and_wait treats JS template characters in the URL as text.""" + _logger.info("testing create_and_wait with template characters...") + + # Test 1: A backtick used to end the template literal in the ready check. + # The resulting SyntaxError was counted as a load, so this returned a tab + with pytest.raises(asyncio.TimeoutError): + await create_and_wait(browser, url="http://192.0.2.1:9999/`", timeout=0.5) + + # Test 2: A ${...} sequence used to run as JavaScript in the page + url = "about:blank?${window.injected=1}" + tab = await create_and_wait(browser, url=url, timeout=5.0) + result = await execute_js_and_wait(tab, "typeof window.injected", timeout=5.0) + assert result["result"]["result"]["value"] == "undefined" + + @pytest.mark.asyncio async def test_navigate_and_wait(browser): """Test navigate_and_wait with both valid data URL and bad URL."""