From a83d6d325c28620bb393d7b79a3cc74fe8130c38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 18:39:45 +0900 Subject: [PATCH 01/15] fix(security): allowlist https://api.github.com before urllib urlopen Semgrep OSS and Bandit B310 Medium alerts on main flagged dynamic urllib use in CodeQL identity and Strix evidence helpers. Fail closed unless the URL is https://api.github.com so file:// and arbitrary hosts cannot reach urlopen. Co-authored-by: Cursor --- scripts/ci/codeql_ghas_configuration_identity.py | 13 ++++++++++++- scripts/ci/strix_evidence_binding.py | 14 +++++++++++++- tests/test_codeql_ghas_configuration_identity.py | 12 ++++++++++++ tests/test_strix_evidence_binding.py | 10 ++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/scripts/ci/codeql_ghas_configuration_identity.py b/scripts/ci/codeql_ghas_configuration_identity.py index 86e2997c8a..1594c2fe3a 100644 --- a/scripts/ci/codeql_ghas_configuration_identity.py +++ b/scripts/ci/codeql_ghas_configuration_identity.py @@ -142,8 +142,19 @@ def format_identity(identity: tuple[str, str]) -> str: return f"{analysis_key} {category}" + +def _assert_github_https_api_url(url: str) -> None: + """Reject non-HTTPS / non-api.github.com URLs before urllib (Semgrep/Bandit B310).""" + parsed = urllib.parse.urlparse(url) + if parsed.scheme != "https" or (parsed.hostname or "").lower() != "api.github.com": + raise ConfigurationIdentityError( + "refusing urllib GET: only https://api.github.com URLs are allowed" + ) + + def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: """GET one GitHub REST URL and decode JSON, or raise ConfigurationIdentityError.""" + _assert_github_https_api_url(url) request = urllib.request.Request( url, headers={ @@ -155,7 +166,7 @@ def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: method="GET", ) try: - with urllib.request.urlopen(request, timeout=timeout_seconds) as response: + with urllib.request.urlopen(request, timeout=timeout_seconds) as response: # noqa: S310 - https api.github.com only payload = response.read().decode("utf-8") except urllib.error.HTTPError as exc: body = exc.read().decode("utf-8", errors="replace")[-400:] diff --git a/scripts/ci/strix_evidence_binding.py b/scripts/ci/strix_evidence_binding.py index eafe777476..2d5001c64a 100644 --- a/scripts/ci/strix_evidence_binding.py +++ b/scripts/ci/strix_evidence_binding.py @@ -27,6 +27,7 @@ from pathlib import Path from typing import Any from urllib.error import HTTPError, URLError +from urllib.parse import urlparse from urllib.request import Request, urlopen @@ -245,11 +246,22 @@ def load_changed_paths_from_github( ) + +def _assert_github_https_api_url(url: str) -> None: + """Reject non-HTTPS / non-api.github.com URLs before urlopen (Semgrep/Bandit B310).""" + parsed = urlparse(url) + if parsed.scheme != "https" or (parsed.hostname or "").lower() != "api.github.com": + raise EvidenceBindingError( + "refusing urllib GET: only https://api.github.com URLs are allowed" + ) + + def default_github_opener(url: str, token: str) -> Any: """Fetch one GitHub API JSON document with a bounded Authorization header.""" if not token: raise EvidenceBindingError("GitHub token is required for changed-file evidence") + _assert_github_https_api_url(url) request = Request( url, headers={ @@ -261,7 +273,7 @@ def default_github_opener(url: str, token: str) -> Any: method="GET", ) try: - with urlopen(request, timeout=30) as response: # noqa: S310 - GitHub HTTPS only + with urlopen(request, timeout=30) as response: # noqa: S310 - https api.github.com only payload = response.read() except HTTPError as exc: raise EvidenceBindingError( diff --git a/tests/test_codeql_ghas_configuration_identity.py b/tests/test_codeql_ghas_configuration_identity.py index 23ca662ea7..202e6a3f88 100644 --- a/tests/test_codeql_ghas_configuration_identity.py +++ b/tests/test_codeql_ghas_configuration_identity.py @@ -495,3 +495,15 @@ def test_list_codeql_analyses_rejects_non_list_payload(monkeypatch): monkeypatch.setattr(identity, "_request_json", lambda url, token, timeout_seconds: {"ok": True}) with pytest.raises(identity.ConfigurationIdentityError): identity.list_codeql_analyses("ContextualWisdomLab/wardnet", token="opaque") + + +def test_request_json_rejects_non_github_https_urls(monkeypatch): + """urllib allowlist must fail closed before urlopen (Semgrep/Bandit Medium).""" + import scripts.ci.codeql_ghas_configuration_identity as mod + calls = [] + monkeypatch.setattr(mod.urllib.request, "urlopen", lambda *a, **k: calls.append((a, k))) + with pytest.raises(mod.ConfigurationIdentityError, match="api.github.com"): + mod._request_json("http://evil.example/x", token="t", timeout_seconds=1) + with pytest.raises(mod.ConfigurationIdentityError, match="api.github.com"): + mod._request_json("https://evil.example/x", token="t", timeout_seconds=1) + assert calls == [] diff --git a/tests/test_strix_evidence_binding.py b/tests/test_strix_evidence_binding.py index 60d3ceb517..093b3f8ce6 100644 --- a/tests/test_strix_evidence_binding.py +++ b/tests/test_strix_evidence_binding.py @@ -969,3 +969,13 @@ def test_workspace_missing_root_returns_false(tmp_path: Path) -> None: missing = tmp_path / "missing-root" assert binding.workspace_contains_expected_diff(missing, "a.py", "body") is False + + +def test_assert_github_https_api_url_allowlist(): + """Only https://api.github.com may reach urlopen in evidence binding.""" + import scripts.ci.strix_evidence_binding as mod + mod._assert_github_https_api_url("https://api.github.com/repos/o/r") + with pytest.raises(mod.EvidenceBindingError, match="api.github.com"): + mod._assert_github_https_api_url("file:///etc/passwd") + with pytest.raises(mod.EvidenceBindingError, match="api.github.com"): + mod._assert_github_https_api_url("https://example.com/x") From 2167daec7c20338c6d8d0473e11e99e9d332732d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 19:46:24 +0900 Subject: [PATCH 02/15] test(codeql): reproduce non-canonical dispatch repository identity --- ...n_dispatch_repository_identity_contract.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/test_codeql_scan_dispatch_repository_identity_contract.py diff --git a/tests/test_codeql_scan_dispatch_repository_identity_contract.py b/tests/test_codeql_scan_dispatch_repository_identity_contract.py new file mode 100644 index 0000000000..5ec1e910bc --- /dev/null +++ b/tests/test_codeql_scan_dispatch_repository_identity_contract.py @@ -0,0 +1,58 @@ +"""Repository-identity admission contract for the CodeQL dispatch handler.""" + +from __future__ import annotations + +import pytest + +from tests.test_codeql_scan_dispatch_workflow_contract import ( + _matching_pull_request, + _run_validate_step, +) + + +def _matching_pull_request_for(repository: str) -> dict: + """Bind the shared live-PR fixture to one target repository identity.""" + pull_request = _matching_pull_request() + pull_request["base"]["repo"]["full_name"] = repository + pull_request["head"]["repo"]["full_name"] = repository + return pull_request + + +@pytest.mark.parametrize( + "repository", + ( + "ContextualWisdomLab/repository.", + "ContextualWisdomLab/repo..name", + "ContextualWisdomLab/..", + "ContextualWisdomLab/.", + ), +) +def test_codeql_scan_dispatch_rejects_noncanonical_target_repository( + tmp_path, repository: str +) -> None: + """Reject non-canonical target slugs in the real validation shell block.""" + result = _run_validate_step( + tmp_path, + {"TARGET_REPOSITORY": repository}, + _matching_pull_request_for(repository), + ) + assert result.returncode != 0 + + +@pytest.mark.parametrize( + "repository", + ( + "ContextualWisdomLab/pg-llm-batch", + "ContextualWisdomLab/repository.name-1", + ), +) +def test_codeql_scan_dispatch_keeps_valid_target_repository( + tmp_path, repository: str +) -> None: + """Preserve valid punctuation-bearing organization-local repository slugs.""" + result = _run_validate_step( + tmp_path, + {"TARGET_REPOSITORY": repository}, + _matching_pull_request_for(repository), + ) + assert result.returncode == 0, result.stderr From 10a87abddddd0b7b02d4c10b4ee9347ebe6f2237 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 19:48:23 +0900 Subject: [PATCH 03/15] test(codeql): pin repository admission failure boundary --- tests/test_codeql_scan_dispatch_repository_identity_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_codeql_scan_dispatch_repository_identity_contract.py b/tests/test_codeql_scan_dispatch_repository_identity_contract.py index 5ec1e910bc..b755817eb8 100644 --- a/tests/test_codeql_scan_dispatch_repository_identity_contract.py +++ b/tests/test_codeql_scan_dispatch_repository_identity_contract.py @@ -37,6 +37,7 @@ def test_codeql_scan_dispatch_rejects_noncanonical_target_repository( _matching_pull_request_for(repository), ) assert result.returncode != 0 + assert "PR metadata validation rejected a target outside ContextualWisdomLab" in result.stderr @pytest.mark.parametrize( From 2b849c874122961e025c29f7fa0bb697863c3d68 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 18 Sep 2026 23:00:44 +0900 Subject: [PATCH 04/15] fix(codeql): reject non-canonical dispatch repositories --- .github/workflows/codeql-scan-dispatch.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index 45cfcc75fc..e2c41fe339 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -263,6 +263,8 @@ jobs: fi if ! [[ "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] || + [[ "${TARGET_REPOSITORY#ContextualWisdomLab/}" == *".."* ]] || + [[ "${TARGET_REPOSITORY#ContextualWisdomLab/}" == *"." ]] || ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then printf '::error::PR metadata validation rejected a target outside ContextualWisdomLab or an invalid pull request number. target=%s pr=%s\n' "${TARGET_REPOSITORY:-}" "${PR_NUMBER:-}" exit 1 From 225260a8f949da525da5ff2190e3b413f41f88c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 05:55:20 +0900 Subject: [PATCH 05/15] test(security): pin GitHub API redirect credential boundary --- ...ql_ghas_configuration_redirect_contract.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_codeql_ghas_configuration_redirect_contract.py diff --git a/tests/test_codeql_ghas_configuration_redirect_contract.py b/tests/test_codeql_ghas_configuration_redirect_contract.py new file mode 100644 index 0000000000..462da45619 --- /dev/null +++ b/tests/test_codeql_ghas_configuration_redirect_contract.py @@ -0,0 +1,60 @@ +"""Credential-egress contract for GHAS configuration-identity HTTP redirects.""" + +from __future__ import annotations + +from email.message import Message +import urllib.request + +import pytest + +from scripts.ci import codeql_ghas_configuration_identity as identity + + +def _redirect_headers(location: str) -> Message: + """Build the header shape urllib passes to ``redirect_request``.""" + headers = Message() + headers["Location"] = location + return headers + + +def test_github_api_redirect_handler_rejects_external_origin_before_forwarding_bearer(): + """An admitted GitHub API request must not redirect its bearer token off-origin.""" + request = urllib.request.Request( + "https://api.github.com/repos/ContextualWisdomLab/.github/code-scanning/analyses", + headers={"Authorization": "Bearer sentinel-secret"}, + method="GET", + ) + handler = identity._GitHubApiRedirectHandler() + + with pytest.raises(identity.ConfigurationIdentityError, match="api.github.com"): + handler.redirect_request( + request, + None, + 302, + "Found", + _redirect_headers("https://evil.example/capture"), + "https://evil.example/capture", + ) + + +def test_github_api_redirect_handler_preserves_same_origin_redirects(): + """Legitimate GitHub API redirects remain usable without weakening the origin boundary.""" + request = urllib.request.Request( + "https://api.github.com/repos/ContextualWisdomLab/.github/code-scanning/analyses", + headers={"Authorization": "Bearer sentinel-secret"}, + method="GET", + ) + handler = identity._GitHubApiRedirectHandler() + + redirected = handler.redirect_request( + request, + None, + 302, + "Found", + _redirect_headers("https://api.github.com/repositories/123/code-scanning/analyses"), + "https://api.github.com/repositories/123/code-scanning/analyses", + ) + + assert redirected is not None + assert redirected.full_url == "https://api.github.com/repositories/123/code-scanning/analyses" + assert redirected.get_header("Authorization") == "Bearer sentinel-secret" From 0ae2204ebcff0441ec5e7ca41ffdd01bdc135a26 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 05:57:57 +0900 Subject: [PATCH 06/15] fix(security): contain GitHub API redirects to admitted origin --- .../ci/codeql_ghas_configuration_identity.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/ci/codeql_ghas_configuration_identity.py b/scripts/ci/codeql_ghas_configuration_identity.py index 1594c2fe3a..e78e9c1865 100644 --- a/scripts/ci/codeql_ghas_configuration_identity.py +++ b/scripts/ci/codeql_ghas_configuration_identity.py @@ -127,8 +127,6 @@ def pairing_ready( category = language_category(language) base_for_language = {item for item in base_ids if item[1] == category} if not base_for_language: - # No base configuration for this language means GHAS will not demand one - # on the head for introduced-alert computation of that language. return True, [] missing = missing_base_identities(base_for_language, head_ids, language=language) return not missing, missing @@ -142,7 +140,6 @@ def format_identity(identity: tuple[str, str]) -> str: return f"{analysis_key} {category}" - def _assert_github_https_api_url(url: str) -> None: """Reject non-HTTPS / non-api.github.com URLs before urllib (Semgrep/Bandit B310).""" parsed = urllib.parse.urlparse(url) @@ -152,6 +149,18 @@ def _assert_github_https_api_url(url: str) -> None: ) +class _GitHubApiRedirectHandler(urllib.request.HTTPRedirectHandler): + """Allow redirects only while the request remains on the GitHub REST origin.""" + + def redirect_request(self, req, fp, code, msg, headers, newurl): + target = urllib.parse.urljoin(req.full_url, newurl) + _assert_github_https_api_url(target) + return super().redirect_request(req, fp, code, msg, headers, target) + + +_GITHUB_API_OPENER = urllib.request.build_opener(_GitHubApiRedirectHandler()) + + def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: """GET one GitHub REST URL and decode JSON, or raise ConfigurationIdentityError.""" _assert_github_https_api_url(url) @@ -166,7 +175,7 @@ def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: method="GET", ) try: - with urllib.request.urlopen(request, timeout=timeout_seconds) as response: # noqa: S310 - https api.github.com only + with _GITHUB_API_OPENER.open(request, timeout=timeout_seconds) as response: payload = response.read().decode("utf-8") except urllib.error.HTTPError as exc: body = exc.read().decode("utf-8", errors="replace")[-400:] @@ -316,4 +325,4 @@ def main(argv: Sequence[str] | None = None) -> int: if __name__ == "__main__": # pragma: no cover - exercised through ``main`` tests - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file From 062663af1f566b4118406e63074750adf950871b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 07:01:11 +0900 Subject: [PATCH 07/15] test(security): pin Strix redirect credential boundary --- ...trix_evidence_binding_redirect_contract.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/test_strix_evidence_binding_redirect_contract.py diff --git a/tests/test_strix_evidence_binding_redirect_contract.py b/tests/test_strix_evidence_binding_redirect_contract.py new file mode 100644 index 0000000000..21a4ddb6df --- /dev/null +++ b/tests/test_strix_evidence_binding_redirect_contract.py @@ -0,0 +1,52 @@ +"""Fail-closed redirect contract for authenticated Strix GitHub API reads.""" + +from __future__ import annotations + +from urllib.request import Request + +import pytest + +from scripts.ci import strix_evidence_binding as binding + + +def _authenticated_request() -> Request: + """Build one admitted GitHub REST request carrying a bearer credential.""" + + return Request( + "https://api.github.com/repos/ContextualWisdomLab/example/pulls/1/files", + headers={"Authorization": "Bearer secret"}, + method="GET", + ) + + +def test_authenticated_redirect_rejects_cross_origin_before_bearer_forwarding() -> None: + """A 30x target outside api.github.com must fail before Request creation.""" + + handler = binding._GitHubApiRedirectHandler() + with pytest.raises(binding.EvidenceBindingError, match="only https://api.github.com"): + handler.redirect_request( + _authenticated_request(), + None, + 302, + "Found", + {}, + "https://evil.example/collect", + ) + + +def test_authenticated_redirect_preserves_same_origin_request() -> None: + """An admitted same-origin redirect keeps the authenticated GitHub request.""" + + handler = binding._GitHubApiRedirectHandler() + redirected = handler.redirect_request( + _authenticated_request(), + None, + 302, + "Found", + {}, + "/repositories/1/pulls/1/files?page=2", + ) + + assert redirected is not None + assert redirected.full_url == "https://api.github.com/repositories/1/pulls/1/files?page=2" + assert redirected.get_header("Authorization") == "Bearer secret" From 2708a6beb69a6cfdb4bdb2ec83eb5383d62d9be4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 07:02:05 +0900 Subject: [PATCH 08/15] fix(security): contain Strix GitHub API redirects --- scripts/ci/strix_evidence_binding.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/scripts/ci/strix_evidence_binding.py b/scripts/ci/strix_evidence_binding.py index 2d5001c64a..30d71ba8a3 100644 --- a/scripts/ci/strix_evidence_binding.py +++ b/scripts/ci/strix_evidence_binding.py @@ -27,8 +27,8 @@ from pathlib import Path from typing import Any from urllib.error import HTTPError, URLError -from urllib.parse import urlparse -from urllib.request import Request, urlopen +from urllib.parse import urljoin, urlparse +from urllib.request import HTTPRedirectHandler, Request, build_opener FULL_SHA_RE = re.compile(r"^[0-9a-f]{40}$") @@ -246,7 +246,6 @@ def load_changed_paths_from_github( ) - def _assert_github_https_api_url(url: str) -> None: """Reject non-HTTPS / non-api.github.com URLs before urlopen (Semgrep/Bandit B310).""" parsed = urlparse(url) @@ -256,6 +255,20 @@ def _assert_github_https_api_url(url: str) -> None: ) +class _GitHubApiRedirectHandler(HTTPRedirectHandler): + """Allow redirects only while an authenticated request remains on GitHub REST.""" + + def redirect_request(self, req, fp, code, msg, headers, newurl): + """Revalidate the target before urllib can copy the Authorization header.""" + + target = urljoin(req.full_url, newurl) + _assert_github_https_api_url(target) + return super().redirect_request(req, fp, code, msg, headers, target) + + +_GITHUB_API_OPENER = build_opener(_GitHubApiRedirectHandler()) + + def default_github_opener(url: str, token: str) -> Any: """Fetch one GitHub API JSON document with a bounded Authorization header.""" @@ -273,7 +286,9 @@ def default_github_opener(url: str, token: str) -> Any: method="GET", ) try: - with urlopen(request, timeout=30) as response: # noqa: S310 - https api.github.com only + with _GITHUB_API_OPENER.open( + request, timeout=30 + ) as response: # noqa: S310 - HTTPS api.github.com only, redirects revalidated payload = response.read() except HTTPError as exc: raise EvidenceBindingError( From 3758b890e012548420da6c3978d3e116ce8b814a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:32:08 +0000 Subject: [PATCH 09/15] chore(deps): bump anyio from 4.14.0 to 4.14.2 Bumps [anyio](https://github.com/agronholm/anyio) from 4.14.0 to 4.14.2. - [Release notes](https://github.com/agronholm/anyio/releases) - [Commits](https://github.com/agronholm/anyio/compare/4.14.0...4.14.2) --- updated-dependencies: - dependency-name: anyio dependency-version: 4.14.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements-strix-ci-hashes.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt index 9e705850b5..eb83beda17 100644 --- a/requirements-strix-ci-hashes.txt +++ b/requirements-strix-ci-hashes.txt @@ -140,9 +140,9 @@ annotated-types==0.7.0 \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 # via pydantic -anyio==4.14.0 \ - --hash=sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89 \ - --hash=sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via # google-genai # gql From 4dcd25c9f2789e4b8acbeef603e118dd80bfa014 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 10:59:36 +0900 Subject: [PATCH 10/15] test(security): align Strix transport seam with dedicated opener --- tests/conftest.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 6f0c91d00f..c87bf8ba46 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ import pytest from scripts.ci import materialize_base_python_requirements as materializer +from scripts.ci import strix_evidence_binding as strix_binding @pytest.fixture(autouse=True) @@ -21,6 +22,26 @@ def clear_trusted_uv_process_caches() -> Iterator[None]: opener_cache_clear() +@pytest.fixture(autouse=True) +def preserve_strix_transport_test_seam( + request: pytest.FixtureRequest, + monkeypatch: pytest.MonkeyPatch, +) -> Iterator[None]: + """Route legacy Strix transport fakes through the production dedicated opener seam.""" + if request.node.path.name != "test_strix_evidence_binding.py": + yield + return + + original_open = strix_binding._GITHUB_API_OPENER.open + monkeypatch.setattr(strix_binding, "urlopen", original_open, raising=False) + monkeypatch.setattr( + strix_binding._GITHUB_API_OPENER, + "open", + lambda *args, **kwargs: strix_binding.urlopen(*args, **kwargs), + ) + yield + + class FakeHttpResponse: """Expose bounded context-managed reads from one deterministic final URL.""" From 834d285f90241b4741247408001fd7534ce5a3b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 19:13:25 +0900 Subject: [PATCH 11/15] test(security): bind authenticated openers at owned seams Replace retired urllib urlopen monkeypatches with direct CodeQL and Strix dedicated-opener patches. Remove the PR-specific global conftest bridge so both security helpers exercise the same explicit transport boundary without live network access. --- tests/conftest.py | 21 ------------------- ...test_codeql_ghas_configuration_identity.py | 16 +++++++------- tests/test_strix_evidence_binding.py | 8 +++---- 3 files changed, 12 insertions(+), 33 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c87bf8ba46..6f0c91d00f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,6 @@ import pytest from scripts.ci import materialize_base_python_requirements as materializer -from scripts.ci import strix_evidence_binding as strix_binding @pytest.fixture(autouse=True) @@ -22,26 +21,6 @@ def clear_trusted_uv_process_caches() -> Iterator[None]: opener_cache_clear() -@pytest.fixture(autouse=True) -def preserve_strix_transport_test_seam( - request: pytest.FixtureRequest, - monkeypatch: pytest.MonkeyPatch, -) -> Iterator[None]: - """Route legacy Strix transport fakes through the production dedicated opener seam.""" - if request.node.path.name != "test_strix_evidence_binding.py": - yield - return - - original_open = strix_binding._GITHUB_API_OPENER.open - monkeypatch.setattr(strix_binding, "urlopen", original_open, raising=False) - monkeypatch.setattr( - strix_binding._GITHUB_API_OPENER, - "open", - lambda *args, **kwargs: strix_binding.urlopen(*args, **kwargs), - ) - yield - - class FakeHttpResponse: """Expose bounded context-managed reads from one deterministic final URL.""" diff --git a/tests/test_codeql_ghas_configuration_identity.py b/tests/test_codeql_ghas_configuration_identity.py index 202e6a3f88..414f654f79 100644 --- a/tests/test_codeql_ghas_configuration_identity.py +++ b/tests/test_codeql_ghas_configuration_identity.py @@ -412,7 +412,7 @@ def fake_urlopen(request, timeout=30): assert "ref=refs%2Fheads%2Fmain" in request.full_url return _Response() - monkeypatch.setattr(identity.urllib.request, "urlopen", fake_urlopen) + monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", fake_urlopen) rows = identity.list_codeql_analyses( "ContextualWisdomLab/wardnet", token="opaque", @@ -437,7 +437,7 @@ def raise_http(request, timeout=30): del request, timeout raise _HTTPError("https://api.github.com/x", 403, "forbidden", hdrs=None, fp=None) - monkeypatch.setattr(identity.urllib.request, "urlopen", raise_http) + monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", raise_http) with pytest.raises(identity.ConfigurationIdentityError) as excinfo: identity._request_json("https://api.github.com/x", token="t", timeout_seconds=1) assert "HTTP 403" in str(excinfo.value) @@ -446,7 +446,7 @@ def raise_url(request, timeout=30): del request, timeout raise identity.urllib.error.URLError("down") - monkeypatch.setattr(identity.urllib.request, "urlopen", raise_url) + monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", raise_url) with pytest.raises(identity.ConfigurationIdentityError): identity._request_json("https://api.github.com/x", token="t", timeout_seconds=1) @@ -465,8 +465,8 @@ def __exit__(self, exc_type, exc, tb) -> None: del exc_type, exc, tb monkeypatch.setattr( - identity.urllib.request, - "urlopen", + identity._GITHUB_API_OPENER, + "open", lambda request, timeout=30: _Empty(), ) assert identity._request_json("https://api.github.com/x", token="t", timeout_seconds=1) == [] @@ -482,8 +482,8 @@ def __exit__(self, exc_type, exc, tb) -> None: del exc_type, exc, tb monkeypatch.setattr( - identity.urllib.request, - "urlopen", + identity._GITHUB_API_OPENER, + "open", lambda request, timeout=30: _Bad(), ) with pytest.raises(identity.ConfigurationIdentityError): @@ -501,7 +501,7 @@ def test_request_json_rejects_non_github_https_urls(monkeypatch): """urllib allowlist must fail closed before urlopen (Semgrep/Bandit Medium).""" import scripts.ci.codeql_ghas_configuration_identity as mod calls = [] - monkeypatch.setattr(mod.urllib.request, "urlopen", lambda *a, **k: calls.append((a, k))) + monkeypatch.setattr(mod._GITHUB_API_OPENER, "open", lambda *a, **k: calls.append((a, k))) with pytest.raises(mod.ConfigurationIdentityError, match="api.github.com"): mod._request_json("http://evil.example/x", token="t", timeout_seconds=1) with pytest.raises(mod.ConfigurationIdentityError, match="api.github.com"): diff --git a/tests/test_strix_evidence_binding.py b/tests/test_strix_evidence_binding.py index 093b3f8ce6..7eb29c9375 100644 --- a/tests/test_strix_evidence_binding.py +++ b/tests/test_strix_evidence_binding.py @@ -658,14 +658,14 @@ def raise_http(*_args: object, **_kwargs: object) -> object: fp=BytesIO(), ) - monkeypatch.setattr(binding, "urlopen", raise_http) + monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", raise_http) with pytest.raises(binding.EvidenceBindingError, match="HTTP 403"): binding.default_github_opener("https://api.github.com/x", "token") def raise_url(*_args: object, **_kwargs: object) -> object: raise binding.URLError("down") - monkeypatch.setattr(binding, "urlopen", raise_url) + monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", raise_url) with pytest.raises(binding.EvidenceBindingError, match="URLError"): binding.default_github_opener("https://api.github.com/x", "token") @@ -687,7 +687,7 @@ def __exit__(self, *_args: object) -> None: return None - monkeypatch.setattr(binding, "urlopen", lambda *_a, **_k: Response()) + monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", lambda *_a, **_k: Response()) with pytest.raises(binding.EvidenceBindingError, match="not JSON"): binding.default_github_opener("https://api.github.com/x", "token") @@ -713,7 +713,7 @@ def __exit__(self, *_args: object) -> None: return None - monkeypatch.setattr(binding, "urlopen", lambda *_a, **_k: Response()) + monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", lambda *_a, **_k: Response()) rows = binding.load_changed_paths_from_github( "https://api.github.com", "ContextualWisdomLab/example", From 9501aea608bc1199c7258e586d387ec05ab96075 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 19:49:10 +0900 Subject: [PATCH 12/15] fix(codeql): emit invalid dispatch identity errors on stderr Integration RED showed the production validation block emitted a GitHub error workflow command on stdout while the hardened admission contract requires a real error stream. Keep fail-closed exit 1 and move only this diagnostic to stderr. --- .github/workflows/codeql-scan-dispatch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index e2c41fe339..b0c4f847f9 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -266,7 +266,7 @@ jobs: [[ "${TARGET_REPOSITORY#ContextualWisdomLab/}" == *".."* ]] || [[ "${TARGET_REPOSITORY#ContextualWisdomLab/}" == *"." ]] || ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then - printf '::error::PR metadata validation rejected a target outside ContextualWisdomLab or an invalid pull request number. target=%s pr=%s\n' "${TARGET_REPOSITORY:-}" "${PR_NUMBER:-}" + printf '::error::PR metadata validation rejected a target outside ContextualWisdomLab or an invalid pull request number. target=%s pr=%s\n' "${TARGET_REPOSITORY:-}" "${PR_NUMBER:-}" >&2 exit 1 fi if [ "$dispatch_protocol" = v2 ] && From 055ee9b9587512fdb999d1c847a4e93828475439 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 19:49:23 +0900 Subject: [PATCH 13/15] test(codeql): assert dispatch admission errors on stderr Retire the stale stdout expectation so both repository-identity contracts pin the fail-closed validation error to stderr. --- tests/test_codeql_scan_dispatch_workflow_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index 3769f48314..6c1f91a9d7 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -724,7 +724,7 @@ def test_codeql_scan_dispatch_validate_step_rejects_non_org_target(tmp_path): ) assert result.returncode == 1 - assert "target outside ContextualWisdomLab" in result.stdout + assert "target outside ContextualWisdomLab" in result.stderr def test_codeql_scan_dispatch_validate_step_rejects_malformed_matrix(tmp_path): From 545648ee56dec2397b88b87a04622d2dc3eae96f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 19 Sep 2026 20:42:54 +0900 Subject: [PATCH 14/15] chore(deps): isolate AnyIO security owner delta Restore the unrelated #2269 URL-opener paths to protected main while retaining the AnyIO 4.14.2 pin and hashes. The URL/redirect responsibility remains in canonical #2279; this PR owns only the dependency security update. Validated with 56 focused tests, 3,335 full tests plus 28 skipped/40 subtests, warnings-as-errors, diff check, and pip-audit reporting no known vulnerabilities. --- .../ci/codeql_ghas_configuration_identity.py | 28 ++------- scripts/ci/strix_evidence_binding.py | 31 +--------- ...test_codeql_ghas_configuration_identity.py | 26 +++----- ...ql_ghas_configuration_redirect_contract.py | 60 ------------------- tests/test_strix_evidence_binding.py | 18 ++---- ...trix_evidence_binding_redirect_contract.py | 52 ---------------- 6 files changed, 17 insertions(+), 198 deletions(-) delete mode 100644 tests/test_codeql_ghas_configuration_redirect_contract.py delete mode 100644 tests/test_strix_evidence_binding_redirect_contract.py diff --git a/scripts/ci/codeql_ghas_configuration_identity.py b/scripts/ci/codeql_ghas_configuration_identity.py index e78e9c1865..86e2997c8a 100644 --- a/scripts/ci/codeql_ghas_configuration_identity.py +++ b/scripts/ci/codeql_ghas_configuration_identity.py @@ -127,6 +127,8 @@ def pairing_ready( category = language_category(language) base_for_language = {item for item in base_ids if item[1] == category} if not base_for_language: + # No base configuration for this language means GHAS will not demand one + # on the head for introduced-alert computation of that language. return True, [] missing = missing_base_identities(base_for_language, head_ids, language=language) return not missing, missing @@ -140,30 +142,8 @@ def format_identity(identity: tuple[str, str]) -> str: return f"{analysis_key} {category}" -def _assert_github_https_api_url(url: str) -> None: - """Reject non-HTTPS / non-api.github.com URLs before urllib (Semgrep/Bandit B310).""" - parsed = urllib.parse.urlparse(url) - if parsed.scheme != "https" or (parsed.hostname or "").lower() != "api.github.com": - raise ConfigurationIdentityError( - "refusing urllib GET: only https://api.github.com URLs are allowed" - ) - - -class _GitHubApiRedirectHandler(urllib.request.HTTPRedirectHandler): - """Allow redirects only while the request remains on the GitHub REST origin.""" - - def redirect_request(self, req, fp, code, msg, headers, newurl): - target = urllib.parse.urljoin(req.full_url, newurl) - _assert_github_https_api_url(target) - return super().redirect_request(req, fp, code, msg, headers, target) - - -_GITHUB_API_OPENER = urllib.request.build_opener(_GitHubApiRedirectHandler()) - - def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: """GET one GitHub REST URL and decode JSON, or raise ConfigurationIdentityError.""" - _assert_github_https_api_url(url) request = urllib.request.Request( url, headers={ @@ -175,7 +155,7 @@ def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: method="GET", ) try: - with _GITHUB_API_OPENER.open(request, timeout=timeout_seconds) as response: + with urllib.request.urlopen(request, timeout=timeout_seconds) as response: payload = response.read().decode("utf-8") except urllib.error.HTTPError as exc: body = exc.read().decode("utf-8", errors="replace")[-400:] @@ -325,4 +305,4 @@ def main(argv: Sequence[str] | None = None) -> int: if __name__ == "__main__": # pragma: no cover - exercised through ``main`` tests - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/scripts/ci/strix_evidence_binding.py b/scripts/ci/strix_evidence_binding.py index 30d71ba8a3..eafe777476 100644 --- a/scripts/ci/strix_evidence_binding.py +++ b/scripts/ci/strix_evidence_binding.py @@ -27,8 +27,7 @@ from pathlib import Path from typing import Any from urllib.error import HTTPError, URLError -from urllib.parse import urljoin, urlparse -from urllib.request import HTTPRedirectHandler, Request, build_opener +from urllib.request import Request, urlopen FULL_SHA_RE = re.compile(r"^[0-9a-f]{40}$") @@ -246,35 +245,11 @@ def load_changed_paths_from_github( ) -def _assert_github_https_api_url(url: str) -> None: - """Reject non-HTTPS / non-api.github.com URLs before urlopen (Semgrep/Bandit B310).""" - parsed = urlparse(url) - if parsed.scheme != "https" or (parsed.hostname or "").lower() != "api.github.com": - raise EvidenceBindingError( - "refusing urllib GET: only https://api.github.com URLs are allowed" - ) - - -class _GitHubApiRedirectHandler(HTTPRedirectHandler): - """Allow redirects only while an authenticated request remains on GitHub REST.""" - - def redirect_request(self, req, fp, code, msg, headers, newurl): - """Revalidate the target before urllib can copy the Authorization header.""" - - target = urljoin(req.full_url, newurl) - _assert_github_https_api_url(target) - return super().redirect_request(req, fp, code, msg, headers, target) - - -_GITHUB_API_OPENER = build_opener(_GitHubApiRedirectHandler()) - - def default_github_opener(url: str, token: str) -> Any: """Fetch one GitHub API JSON document with a bounded Authorization header.""" if not token: raise EvidenceBindingError("GitHub token is required for changed-file evidence") - _assert_github_https_api_url(url) request = Request( url, headers={ @@ -286,9 +261,7 @@ def default_github_opener(url: str, token: str) -> Any: method="GET", ) try: - with _GITHUB_API_OPENER.open( - request, timeout=30 - ) as response: # noqa: S310 - HTTPS api.github.com only, redirects revalidated + with urlopen(request, timeout=30) as response: # noqa: S310 - GitHub HTTPS only payload = response.read() except HTTPError as exc: raise EvidenceBindingError( diff --git a/tests/test_codeql_ghas_configuration_identity.py b/tests/test_codeql_ghas_configuration_identity.py index 414f654f79..23ca662ea7 100644 --- a/tests/test_codeql_ghas_configuration_identity.py +++ b/tests/test_codeql_ghas_configuration_identity.py @@ -412,7 +412,7 @@ def fake_urlopen(request, timeout=30): assert "ref=refs%2Fheads%2Fmain" in request.full_url return _Response() - monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", fake_urlopen) + monkeypatch.setattr(identity.urllib.request, "urlopen", fake_urlopen) rows = identity.list_codeql_analyses( "ContextualWisdomLab/wardnet", token="opaque", @@ -437,7 +437,7 @@ def raise_http(request, timeout=30): del request, timeout raise _HTTPError("https://api.github.com/x", 403, "forbidden", hdrs=None, fp=None) - monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", raise_http) + monkeypatch.setattr(identity.urllib.request, "urlopen", raise_http) with pytest.raises(identity.ConfigurationIdentityError) as excinfo: identity._request_json("https://api.github.com/x", token="t", timeout_seconds=1) assert "HTTP 403" in str(excinfo.value) @@ -446,7 +446,7 @@ def raise_url(request, timeout=30): del request, timeout raise identity.urllib.error.URLError("down") - monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", raise_url) + monkeypatch.setattr(identity.urllib.request, "urlopen", raise_url) with pytest.raises(identity.ConfigurationIdentityError): identity._request_json("https://api.github.com/x", token="t", timeout_seconds=1) @@ -465,8 +465,8 @@ def __exit__(self, exc_type, exc, tb) -> None: del exc_type, exc, tb monkeypatch.setattr( - identity._GITHUB_API_OPENER, - "open", + identity.urllib.request, + "urlopen", lambda request, timeout=30: _Empty(), ) assert identity._request_json("https://api.github.com/x", token="t", timeout_seconds=1) == [] @@ -482,8 +482,8 @@ def __exit__(self, exc_type, exc, tb) -> None: del exc_type, exc, tb monkeypatch.setattr( - identity._GITHUB_API_OPENER, - "open", + identity.urllib.request, + "urlopen", lambda request, timeout=30: _Bad(), ) with pytest.raises(identity.ConfigurationIdentityError): @@ -495,15 +495,3 @@ def test_list_codeql_analyses_rejects_non_list_payload(monkeypatch): monkeypatch.setattr(identity, "_request_json", lambda url, token, timeout_seconds: {"ok": True}) with pytest.raises(identity.ConfigurationIdentityError): identity.list_codeql_analyses("ContextualWisdomLab/wardnet", token="opaque") - - -def test_request_json_rejects_non_github_https_urls(monkeypatch): - """urllib allowlist must fail closed before urlopen (Semgrep/Bandit Medium).""" - import scripts.ci.codeql_ghas_configuration_identity as mod - calls = [] - monkeypatch.setattr(mod._GITHUB_API_OPENER, "open", lambda *a, **k: calls.append((a, k))) - with pytest.raises(mod.ConfigurationIdentityError, match="api.github.com"): - mod._request_json("http://evil.example/x", token="t", timeout_seconds=1) - with pytest.raises(mod.ConfigurationIdentityError, match="api.github.com"): - mod._request_json("https://evil.example/x", token="t", timeout_seconds=1) - assert calls == [] diff --git a/tests/test_codeql_ghas_configuration_redirect_contract.py b/tests/test_codeql_ghas_configuration_redirect_contract.py deleted file mode 100644 index 462da45619..0000000000 --- a/tests/test_codeql_ghas_configuration_redirect_contract.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Credential-egress contract for GHAS configuration-identity HTTP redirects.""" - -from __future__ import annotations - -from email.message import Message -import urllib.request - -import pytest - -from scripts.ci import codeql_ghas_configuration_identity as identity - - -def _redirect_headers(location: str) -> Message: - """Build the header shape urllib passes to ``redirect_request``.""" - headers = Message() - headers["Location"] = location - return headers - - -def test_github_api_redirect_handler_rejects_external_origin_before_forwarding_bearer(): - """An admitted GitHub API request must not redirect its bearer token off-origin.""" - request = urllib.request.Request( - "https://api.github.com/repos/ContextualWisdomLab/.github/code-scanning/analyses", - headers={"Authorization": "Bearer sentinel-secret"}, - method="GET", - ) - handler = identity._GitHubApiRedirectHandler() - - with pytest.raises(identity.ConfigurationIdentityError, match="api.github.com"): - handler.redirect_request( - request, - None, - 302, - "Found", - _redirect_headers("https://evil.example/capture"), - "https://evil.example/capture", - ) - - -def test_github_api_redirect_handler_preserves_same_origin_redirects(): - """Legitimate GitHub API redirects remain usable without weakening the origin boundary.""" - request = urllib.request.Request( - "https://api.github.com/repos/ContextualWisdomLab/.github/code-scanning/analyses", - headers={"Authorization": "Bearer sentinel-secret"}, - method="GET", - ) - handler = identity._GitHubApiRedirectHandler() - - redirected = handler.redirect_request( - request, - None, - 302, - "Found", - _redirect_headers("https://api.github.com/repositories/123/code-scanning/analyses"), - "https://api.github.com/repositories/123/code-scanning/analyses", - ) - - assert redirected is not None - assert redirected.full_url == "https://api.github.com/repositories/123/code-scanning/analyses" - assert redirected.get_header("Authorization") == "Bearer sentinel-secret" diff --git a/tests/test_strix_evidence_binding.py b/tests/test_strix_evidence_binding.py index 7eb29c9375..60d3ceb517 100644 --- a/tests/test_strix_evidence_binding.py +++ b/tests/test_strix_evidence_binding.py @@ -658,14 +658,14 @@ def raise_http(*_args: object, **_kwargs: object) -> object: fp=BytesIO(), ) - monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", raise_http) + monkeypatch.setattr(binding, "urlopen", raise_http) with pytest.raises(binding.EvidenceBindingError, match="HTTP 403"): binding.default_github_opener("https://api.github.com/x", "token") def raise_url(*_args: object, **_kwargs: object) -> object: raise binding.URLError("down") - monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", raise_url) + monkeypatch.setattr(binding, "urlopen", raise_url) with pytest.raises(binding.EvidenceBindingError, match="URLError"): binding.default_github_opener("https://api.github.com/x", "token") @@ -687,7 +687,7 @@ def __exit__(self, *_args: object) -> None: return None - monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", lambda *_a, **_k: Response()) + monkeypatch.setattr(binding, "urlopen", lambda *_a, **_k: Response()) with pytest.raises(binding.EvidenceBindingError, match="not JSON"): binding.default_github_opener("https://api.github.com/x", "token") @@ -713,7 +713,7 @@ def __exit__(self, *_args: object) -> None: return None - monkeypatch.setattr(binding._GITHUB_API_OPENER, "open", lambda *_a, **_k: Response()) + monkeypatch.setattr(binding, "urlopen", lambda *_a, **_k: Response()) rows = binding.load_changed_paths_from_github( "https://api.github.com", "ContextualWisdomLab/example", @@ -969,13 +969,3 @@ def test_workspace_missing_root_returns_false(tmp_path: Path) -> None: missing = tmp_path / "missing-root" assert binding.workspace_contains_expected_diff(missing, "a.py", "body") is False - - -def test_assert_github_https_api_url_allowlist(): - """Only https://api.github.com may reach urlopen in evidence binding.""" - import scripts.ci.strix_evidence_binding as mod - mod._assert_github_https_api_url("https://api.github.com/repos/o/r") - with pytest.raises(mod.EvidenceBindingError, match="api.github.com"): - mod._assert_github_https_api_url("file:///etc/passwd") - with pytest.raises(mod.EvidenceBindingError, match="api.github.com"): - mod._assert_github_https_api_url("https://example.com/x") diff --git a/tests/test_strix_evidence_binding_redirect_contract.py b/tests/test_strix_evidence_binding_redirect_contract.py deleted file mode 100644 index 21a4ddb6df..0000000000 --- a/tests/test_strix_evidence_binding_redirect_contract.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Fail-closed redirect contract for authenticated Strix GitHub API reads.""" - -from __future__ import annotations - -from urllib.request import Request - -import pytest - -from scripts.ci import strix_evidence_binding as binding - - -def _authenticated_request() -> Request: - """Build one admitted GitHub REST request carrying a bearer credential.""" - - return Request( - "https://api.github.com/repos/ContextualWisdomLab/example/pulls/1/files", - headers={"Authorization": "Bearer secret"}, - method="GET", - ) - - -def test_authenticated_redirect_rejects_cross_origin_before_bearer_forwarding() -> None: - """A 30x target outside api.github.com must fail before Request creation.""" - - handler = binding._GitHubApiRedirectHandler() - with pytest.raises(binding.EvidenceBindingError, match="only https://api.github.com"): - handler.redirect_request( - _authenticated_request(), - None, - 302, - "Found", - {}, - "https://evil.example/collect", - ) - - -def test_authenticated_redirect_preserves_same_origin_request() -> None: - """An admitted same-origin redirect keeps the authenticated GitHub request.""" - - handler = binding._GitHubApiRedirectHandler() - redirected = handler.redirect_request( - _authenticated_request(), - None, - 302, - "Found", - {}, - "/repositories/1/pulls/1/files?page=2", - ) - - assert redirected is not None - assert redirected.full_url == "https://api.github.com/repositories/1/pulls/1/files?page=2" - assert redirected.get_header("Authorization") == "Bearer secret" From 8a5251bf409fe84b3dd0cba1e48992f5b8d9eda5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 20 Sep 2026 10:02:09 +0900 Subject: [PATCH 15/15] fix(deps): restore AnyIO owner isolation