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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ Keep a Changelog, and releases use semantic versioning.
ownership regression and aligned architecture and operator guidance without
restoring local merge automation or changing production identity behavior.

- Upgraded the development HTTP transport `httpx2` and its exact-coupled
`httpcore2` from 2.9.1 to 2.12.0 on `pyproject.toml`, `uv.lock`, and
`requirements-dev.txt` together, closing the HIGH advisories
CVE-2026-84381 (`wss` through SOCKS5 sent without TLS) and CVE-2026-84382
(streaming decompression memory amplification) that failed the `trivy-fs`
security gate, with a lock-floor regression test on every surface.
- Prevented relying-party inventory from silently accepting a KV key/body
identity mismatch, rejected unsafe live or `Location`-derived client UUIDs,
and aligned exact client discovery with Keycloak's documented
Expand Down
84 changes: 84 additions & 0 deletions docs/doctoring/2026-10-httpx2-transport-security-floor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# October 2026 HTTPX2 Transport Security Floor — Doctoring Record

## Scope

The `trivy-fs` security gate reported three HIGH findings in
`services/account_unification/uv.lock`. All of them were in the development
HTTP transport. That transport consists of the direct development requirement
`httpx2` and its exact-coupled dependency `httpcore2`, both at 2.9.1:

| Advisory | Affected | Fixed | Effect |
|---|---|---|---|
| CVE-2026-84381 / GHSA-7mj9-2mp8-4m2p | `httpcore2` < 2.10.0; `httpx2` 2.6.0–2.9.1 | 2.10.0 | A `wss` origin through a SOCKS5 proxy does not start TLS. The handshake, headers, cookies, and frames cross the proxy in cleartext without certificate verification. |
| CVE-2026-84382 / GHSA-8xx6-hgc6-gc2m | `httpx2` < 2.12.0 | 2.12.0 | Each compressed network chunk is fully inflated before bounded streaming, so a hostile server can force very large intermediate allocations. |

This change moves both packages to 2.12.0 on the three canonical dependency
surfaces in one resolver execution. `httpx2` 2.12.0 requires
`httpcore2==2.12.0`. The resolver also adds `httpx2-jsfetch` 1.0
(BSD-3-Clause), but only behind an `emscripten` platform marker. It is not
installed on Linux or macOS.

No application code, database schema, authentication policy, review
credential, or LLM integration changes. The account-unification runtime
requirement set (`requirements.lock`) does not contain `httpx2`. The packages
are test and development transport only. This lowers runtime exposure, but it
does not remove the need for a patched development graph, because the tests
and CI runners execute this transport.

## Separate Dependabot proposals

Dependabot opened separate drafts for each package (#122, #124, #149,
#150). `httpx2` pins `httpcore2` exactly, so a fragment that updates one
package without the other cannot resolve. A `requirements-dev.txt` edit
without the matching `uv.lock` change is also not reproducible. This change
replaces those fragments with one coupled update. The behavior is consistent
with the grouped-update rationale in
`2026-08-dependency-and-packaging-refresh.md`.

## Verification contract

`tests/test_dependency_security_floor.py` reads `pyproject.toml`, `uv.lock`,
and `requirements-dev.txt` directly. It preserves every matching package
entry and fails when any resolved or generated pin for `httpx2` or
`httpcore2` is below 2.12.0, or when the two lock representations disagree.
A baseline-negative control temporarily restored the dependency surfaces from
`main` after the test was written and produced the intended failures; the
original implementation order was dependency update, test creation, RED
control, then restored patched surfaces and GREEN. This was not an original
production-code tests-first sequence.

The patched graph was then verified with `uv lock --check`, a locked Python
3.12.13 install, Ruff, 100% docstring coverage, compilation, documentation
contracts, 765 passing tests, 100% production statement/branch coverage, and
a local HIGH/CRITICAL Trivy scan with zero findings. An online `uv build`
Comment on lines +50 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Exact-head coverage remains unverified

The cited full-suite coverage predates the final commit. The repository requires 100% production statement and branch coverage on the exact head.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

attempt failed because the PyPI connection timed out while resolving the
already pinned build requirement `setuptools==84.0.0`; this is an environment
network failure, not a source or lock failure. The same build completed
successfully with `uv build --offline` from the retained local package cache.
These checks establish graph compatibility and package-surface consistency;
they do not claim that this repository executes HTTPX2 request or WebSocket
traffic.

## Residual risk

A security floor proves that a known vulnerable release is not selected. It
does not prove that 2.12.0 has no other defects. Hosted `trivy-fs` and
dependency-review results on the exact pull request head remain
authoritative.

## References — APA 7th

Aqua Security. (2026a). *CVE-2026-84381*. Aqua Vulnerability Database.
Retrieved October 6, 2026, from https://avd.aquasec.com/nvd/cve-2026-84381

Aqua Security. (2026b). *CVE-2026-84382*. Aqua Vulnerability Database.
Retrieved October 6, 2026, from https://avd.aquasec.com/nvd/cve-2026-84382

Astral Software, Inc. (2026). *Locking and syncing*. Retrieved August 6,
2026, from https://docs.astral.sh/uv/concepts/projects/sync/

Pydantic. (2026a). *HTTPX2 2.10.0* [Software release]. GitHub.
https://github.com/pydantic/httpx2/releases/tag/v2.10.0

Pydantic. (2026b). *HTTPX2 2.12.0* [Software release]. GitHub.
https://github.com/pydantic/httpx2/releases/tag/v2.12.0
2 changes: 1 addition & 1 deletion services/account_unification/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies = [
[project.optional-dependencies]
dev = [
"coverage==7.15.4",
"httpx2==2.9.1",
"httpx2==2.12.0",
"interrogate==1.7.0",
"pytest==9.1.1",
"ruff==0.16.3",
Expand Down
18 changes: 11 additions & 7 deletions services/account_unification/requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -300,18 +300,22 @@ httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httpcore2==2.9.1 \
--hash=sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2 \
--hash=sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26
httpcore2==2.12.0 ; sys_platform != 'emscripten' \
--hash=sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb \
--hash=sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648
# via httpx2
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via cwl-idp-account-unification
httpx2==2.9.1 \
--hash=sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a \
--hash=sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a
httpx2==2.12.0 \
--hash=sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf \
--hash=sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36
# via cwl-idp-account-unification
httpx2-jsfetch==1.0 ; python_full_version >= '3.12' and sys_platform == 'emscripten' \
--hash=sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60 \
--hash=sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32
# via httpx2
idna==3.18 \
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
Expand Down Expand Up @@ -533,7 +537,7 @@ tabulate==0.10.0 \
--hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \
--hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3
# via interrogate
truststore==0.10.4 \
truststore==0.10.4 ; sys_platform != 'emscripten' \
--hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \
--hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981
# via
Expand Down
118 changes: 118 additions & 0 deletions services/account_unification/tests/test_dependency_security_floor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Locked dependency surfaces stay above published security fix versions.

The development dependency graph contains ``httpx2`` and its exact-coupled
``httpcore2``. The graph had two HIGH advisories: CVE-2026-84381 (``wss``
through SOCKS5 sent in cleartext, fixed in 2.10.0) and CVE-2026-84382
(streaming decompression memory amplification, fixed in 2.12.0). These tests
read every matching entry from the reviewed declaration, the universal
``uv.lock`` resolution, and the generated hash-locked development
requirements. They verify the dependency graph, not execution of HTTPX2
request or WebSocket consumers.
"""
from __future__ import annotations

import re
import tomllib
from pathlib import Path

import pytest

SERVICE_ROOT = Path(__file__).resolve().parents[1]

SECURITY_FLOORS = {
"httpx2": (2, 12, 0),
"httpcore2": (2, 12, 0),
}


def _version_tuple(version: str) -> tuple[int, ...]:
"""Convert a plain release version such as ``2.12.0`` into comparable integers."""
return tuple(int(part) for part in version.split("."))


def _versions_from_lock(
lock: dict[str, list[dict[str, str]]],
) -> dict[str, list[str]]:
"""Collect every resolved version, including platform-specific lock rows."""
versions: dict[str, list[str]] = {}
for package in lock["package"]:
versions.setdefault(package["name"], []).append(package["version"])
return versions


def _versions_from_requirements(text: str) -> dict[str, list[str]]:
"""Collect every exact requirement pin, including conditional pins."""
versions: dict[str, list[str]] = {}
for match in re.finditer(
r"^([A-Za-z0-9_.-]+)==([0-9.]+)", text, re.MULTILINE
):
versions.setdefault(match.group(1).lower(), []).append(match.group(2))
return versions


def _locked_versions() -> dict[str, list[str]]:
"""Return every resolved version for each package name in ``uv.lock``."""
lock = tomllib.loads((SERVICE_ROOT / "uv.lock").read_text(encoding="utf-8"))
return _versions_from_lock(lock)


def _requirements_dev_versions() -> dict[str, list[str]]:
"""Return every exact pin for each package in generated dev requirements."""
text = (SERVICE_ROOT / "requirements-dev.txt").read_text(encoding="utf-8")
return _versions_from_requirements(text)


def _assert_security_floor(
package_name: str, versions: dict[str, list[str]]
) -> None:
"""Reject every package version below its published security floor."""
assert package_name in versions
assert all(
_version_tuple(version) >= SECURITY_FLOORS[package_name]
for version in versions[package_name]
)


def test_security_floor_checks_every_conditional_requirement_pin() -> None:
"""A safe final pin must not hide an earlier vulnerable conditional pin."""
versions = _versions_from_requirements(
"""
httpx2==2.9.1 ; sys_platform == 'win32'
httpx2==2.12.0 ; sys_platform != 'win32'
httpcore2==2.9.1 ; sys_platform == 'win32'
httpcore2==2.12.0 ; sys_platform != 'win32'
"""
)
assert versions["httpx2"] == ["2.9.1", "2.12.0"]
assert versions["httpcore2"] == ["2.9.1", "2.12.0"]
with pytest.raises(AssertionError):
_assert_security_floor("httpx2", versions)
with pytest.raises(AssertionError):
_assert_security_floor("httpcore2", versions)


@pytest.mark.parametrize("package_name", sorted(SECURITY_FLOORS))
def test_universal_lock_resolves_patched_transport(package_name: str) -> None:
"""``uv.lock`` must not resolve a release affected by the HTTPX2 advisories."""
_assert_security_floor(package_name, _locked_versions())


@pytest.mark.parametrize("package_name", sorted(SECURITY_FLOORS))
def test_generated_dev_requirements_pin_patched_transport(package_name: str) -> None:
"""The hash-locked pip surface must agree with the patched universal lock."""
pinned = _requirements_dev_versions()
locked = _locked_versions()
assert pinned[package_name] == locked[package_name]
_assert_security_floor(package_name, pinned)


def test_direct_declaration_pins_patched_httpx2() -> None:
"""The reviewed ``pyproject.toml`` dev pin is the patched release, not the lock alone."""
project = tomllib.loads((SERVICE_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
pins = [
requirement
for requirement in project["project"]["optional-dependencies"]["dev"]
if requirement.startswith("httpx2==")
]
assert len(pins) == 1
assert _version_tuple(pins[0].split("==", 1)[1]) >= SECURITY_FLOORS["httpx2"]
34 changes: 24 additions & 10 deletions services/account_unification/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading