This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/sbp by this push:
     new 5d3140b0 Fix the encoding of JSON data in the form to move files
5d3140b0 is described below

commit 5d3140b086f566f0b1555f3c18242396a29072fd
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Feb 20 16:24:41 2026 +0000

    Fix the encoding of JSON data in the form to move files
---
 atr/get/finish.py              |  5 ++---
 atr/util.py                    |  6 ++++++
 tests/e2e/announce/conftest.py | 10 ++++++++++
 tests/e2e/announce/test_get.py | 13 +++++++++++++
 tests/unit/test_util.py        | 13 +++++++++++++
 5 files changed, 44 insertions(+), 3 deletions(-)

diff --git a/atr/get/finish.py b/atr/get/finish.py
index 61fabc75..39d36907 100644
--- a/atr/get/finish.py
+++ b/atr/get/finish.py
@@ -17,7 +17,6 @@
 
 
 import dataclasses
-import json
 import pathlib
 from collections.abc import Sequence
 
@@ -440,9 +439,9 @@ async def _render_page(
     safe_source_files_rel = [util.validate_path(f).as_posix() for f in 
sorted(source_files_rel)]
     safe_target_dirs = [util.validate_path(d).as_posix() for d in 
sorted(target_dirs)]
     page.append(
-        htpy.script(id="file-data", 
type="application/json")[markupsafe.escape(json.dumps(safe_source_files_rel))]
+        htpy.script(id="file-data", 
type="application/json")[util.json_for_script_element(safe_source_files_rel)]
     )
-    page.append(htpy.script(id="dir-data", 
type="application/json")[markupsafe.escape(json.dumps(safe_target_dirs))])
+    page.append(htpy.script(id="dir-data", 
type="application/json")[util.json_for_script_element(safe_target_dirs)])
     page.append(
         htpy.script(
             id="main-script-data",
diff --git a/atr/util.py b/atr/util.py
index 3730c62b..1c30d07b 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -44,6 +44,7 @@ import asfquart.base as base
 import asfquart.session as session
 import gitignore_parser
 import jinja2
+import markupsafe
 import quart
 
 # NOTE: The atr.db module imports this module
@@ -139,6 +140,11 @@ def as_url(func: Callable, **kwargs: Any) -> str:
     return quart.url_for(annotations["endpoint"], **kwargs)
 
 
+def json_for_script_element(value: Any) -> markupsafe.Markup:
+    """Serialise JSON safely for use inside a script element."""
+    return jinja2.utils.htmlsafe_json_dumps(value, dumps=json.dumps, 
ensure_ascii=False)
+
+
 def asf_uid_from_email(email: str) -> str | None:
     ldap_params = ldap.SearchParameters(email_query=email)
     ldap.search(ldap_params)
diff --git a/tests/e2e/announce/conftest.py b/tests/e2e/announce/conftest.py
index 33f3f7cd..71a14105 100644
--- a/tests/e2e/announce/conftest.py
+++ b/tests/e2e/announce/conftest.py
@@ -35,6 +35,7 @@ VERSION_NAME: Final[str] = "0.1+e2e-announce"
 FILE_NAME: Final[str] = "apache-test-0.2.tar.gz"
 CURRENT_DIR: Final[pathlib.Path] = pathlib.Path(__file__).parent.resolve()
 ANNOUNCE_URL: Final[str] = f"/announce/{PROJECT_NAME}/{VERSION_NAME}"
+FINISH_URL: Final[str] = f"/finish/{PROJECT_NAME}/{VERSION_NAME}"
 
 
 @pytest.fixture(scope="module")
@@ -104,6 +105,15 @@ def page_announce(announce_context: BrowserContext) -> 
Generator[Page]:
     page.close()
 
 
[email protected]
+def page_finish(announce_context: BrowserContext) -> Generator[Page]:
+    """Navigate to the finish page with a fresh page for each test."""
+    page = announce_context.new_page()
+    helpers.visit(page, FINISH_URL)
+    yield page
+    page.close()
+
+
 def _poll_for_vote_thread_link(page: Page, max_attempts: int = 30) -> None:
     """Poll for the vote task to be completed."""
     thread_link_locator = page.locator('a:has-text("view thread")')
diff --git a/tests/e2e/announce/test_get.py b/tests/e2e/announce/test_get.py
index 56cf8000..3390fa5b 100644
--- a/tests/e2e/announce/test_get.py
+++ b/tests/e2e/announce/test_get.py
@@ -82,3 +82,16 @@ def 
test_submit_button_disabled_until_confirm_typed(page_announce: Page) -> None
 
     confirm_input.fill("CONFIRM")
     expect(submit_button).to_be_enabled()
+
+
+def test_finish_move_form_populates_from_json(page_finish: Page) -> None:
+    """The finish move form should populate rows from script JSON data."""
+    file_option = page_finish.locator("#file-list-table-body 
input[type='checkbox'][data-item-path]").first
+    dir_option = page_finish.locator("#dir-list-table-body 
input[type='radio'][name='target-directory-radio']").first
+    expect(file_option).to_be_visible()
+    expect(dir_option).to_be_visible()
+
+    toggle_button = page_finish.locator("#select-files-toggle-button")
+    expect(toggle_button).to_have_text("Select these files")
+    toggle_button.click()
+    expect(toggle_button).to_have_text("Unselect all")
diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py
index 704f402d..454350c9 100644
--- a/tests/unit/test_util.py
+++ b/tests/unit/test_util.py
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import json
 import os
 import pathlib
 import stat
@@ -93,3 +94,15 @@ def test_chmod_files_sets_default_permissions(tmp_path: 
pathlib.Path):
 
     file_mode = stat.S_IMODE(test_file.stat().st_mode)
     assert file_mode == 0o444
+
+
+def test_json_for_script_element_escapes_correctly():
+    payload = ["example.txt", "</script><script>alert(1)</script>", 
"apple&banana"]
+
+    serialized = util.json_for_script_element(payload)
+
+    assert "</script>" not in serialized
+    assert "<script>" not in serialized
+    assert "apple&banana" not in serialized
+    assert "apple\\u0026banana" in serialized
+    assert json.loads(serialized) == payload


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to