diff --git a/.github/scripts/verify_sanitized_artifacts.py b/.github/scripts/verify_sanitized_artifacts.py
index be177b5e5..cd2959b1f 100755
--- a/.github/scripts/verify_sanitized_artifacts.py
+++ b/.github/scripts/verify_sanitized_artifacts.py
@@ -57,6 +57,8 @@ ALLOWED_ATTRIBUTES_BY_TAG = {
"skipped": frozenset({"message", "type"}),
}
+SAFE_SKIPPED_STATUSES = frozenset({"skipped", "todo", "ignored", "disabled", "pending", "omitted", "test skipped"})
+
SENSITIVE_KEY_PATTERN = re.compile(
r"(?i)(password|passwd|secret|token|api_key|apikey|access_token|refresh_token|"
r"private_key|auth_token|client_secret|session|session_id|sid|csrf|xsrf|jwt|credential|authorization)"
@@ -186,6 +188,15 @@ def prune_and_sanitize_junit_element(elem: ET.Element) -> bool:
if attr_lower not in allowed_attrs:
del elem.attrib[attr_name]
modified = True
+ elif tag_lower == "skipped":
+ val = elem.attrib[attr_name].strip()
+ if val.lower() in SAFE_SKIPPED_STATUSES:
+ if elem.attrib[attr_name] != val.lower():
+ elem.attrib[attr_name] = val.lower()
+ modified = True
+ else:
+ elem.attrib[attr_name] = "skipped"
+ modified = True
else:
val = elem.attrib[attr_name]
if isinstance(val, str):
@@ -194,9 +205,9 @@ def prune_and_sanitize_junit_element(elem: ET.Element) -> bool:
elem.attrib[attr_name] = new_val
modified = True
- # 3. Handle text content: structural elements do not carry free-form text
- if tag_lower in ("testsuites", "testsuite", "testcase"):
- if elem.text and elem.text.strip():
+ # 3. Handle text content: structural and skipped elements do not carry free-form text
+ if tag_lower in ("testsuites", "testsuite", "testcase", "skipped"):
+ if elem.text:
elem.text = None
modified = True
elif tag_lower in ("failure", "error"):
@@ -206,11 +217,11 @@ def prune_and_sanitize_junit_element(elem: ET.Element) -> bool:
elem.text = new_text
modified = True
- # Clean up tails
+ # 4. Clean up tails: strip any non-whitespace characters from tails to prevent leaks
if elem.tail and elem.tail.strip():
- new_tail, changed = sanitize_text_content(elem.tail)
- if changed:
- elem.tail = new_tail
+ cleaned_tail = re.sub(r"\S+", "", elem.tail)
+ if cleaned_tail != elem.tail:
+ elem.tail = cleaned_tail
modified = True
return modified
diff --git a/tests/test_verify_sanitized_artifacts.py b/tests/test_verify_sanitized_artifacts.py
index 488873437..17dfb0098 100644
--- a/tests/test_verify_sanitized_artifacts.py
+++ b/tests/test_verify_sanitized_artifacts.py
@@ -250,6 +250,50 @@ def test_standard_node_and_playwright_reports_remain_valid_and_retain_identities
assert "session_token" not in pw_text
+def test_skipped_element_content_tail_and_attributes_leak_prevented(tmp_path):
+ """Regression: PRIVATE_VALUE_123, attributes, and tails cannot leak sensitive values."""
+ results_dir = tmp_path / "test-results"
+ results_dir.mkdir()
+ junit = results_dir / "browser-junit.xml"
+ xml_with_skipped = """
+
+
+ PRIVATE_VALUE_123PRIVATE_VALUE_123
+
+
+"""
+ junit.write_text(xml_with_skipped)
+
+ ok = verify_and_sanitize_artifacts(str(results_dir))
+ assert ok is True
+ assert junit.exists()
+
+ sanitized_xml = junit.read_text()
+ # Synthetic sensitive values that do not match secret regexes must be absent
+ assert "PRIVATE_VALUE_123" not in sanitized_xml
+ assert "UNOFFICIAL_SECRET_456" not in sanitized_xml
+
+ # The output must remain valid JUnit XML
+ tree = ET.parse(junit)
+ root = tree.getroot()
+ assert root.tag == "testsuites"
+ cases = list(root.iter("testcase"))
+ assert len(cases) == 1
+
+ # Testcase identity remains intact
+ testcase = cases[0]
+ assert testcase.get("name") == "skipped-case"
+ assert testcase.get("classname") == "sample"
+
+ # Skipped element remains present and classified as skipped
+ skipped_elems = list(testcase.iter("skipped"))
+ assert len(skipped_elems) == 1
+ skipped_elem = skipped_elems[0]
+ assert skipped_elem.text is None or not skipped_elem.text.strip()
+ assert skipped_elem.get("message") == "skipped"
+ assert skipped_elem.get("type") == "skipped"
+
+
def test_unrecognized_xml_purged(tmp_path):
"""Negative control: XML reports that are not recognized JUnit reports are purged."""
results_dir = tmp_path / "test-results"