This is an automated email from the ASF dual-hosted git repository. imbajin pushed a commit to branch feat/oink-community-content in repository https://gitbox.apache.org/repos/asf/hugegraph-doc.git
commit 366e209e60493812f5ff9c0c9016d56149cf1cbb Author: dark <[email protected]> AuthorDate: Fri Sep 4 19:08:15 2026 +0800 fix(community): enforce offline avatar validation - strip optional WebP metadata before content hashing\n- reject metadata-bearing checked-in avatar files\n- disable module network access during rendered-output checks\n- cover WebP metadata removal deterministically --- scripts/community_roster.py | 36 ++++++++++++++++++++++++++++++++++-- scripts/test_community_roster.py | 9 +++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/scripts/community_roster.py b/scripts/community_roster.py index ff9d2bb36..817d35adb 100644 --- a/scripts/community_roster.py +++ b/scripts/community_roster.py @@ -110,13 +110,36 @@ def _webp_dimensions(raw: bytes) -> tuple[int, int]: raise RosterError(f"avatar uses unsupported WebP chunk {chunk!r}") +def _strip_webp_metadata(raw: bytes) -> bytes: + """Remove optional metadata chunks while preserving the image bitstream.""" + _webp_dimensions(raw) + chunks: list[bytes] = [] + cursor = 12 + while cursor + 8 <= len(raw): + kind = raw[cursor : cursor + 4] + size = int.from_bytes(raw[cursor + 4 : cursor + 8], "little") + end = cursor + 8 + size + (size % 2) + if end > len(raw): + raise RosterError("avatar has a truncated WebP chunk") + chunk = bytearray(raw[cursor:end]) + if kind not in {b"EXIF", b"XMP ", b"ICCP"}: + if kind == b"VP8X": + chunk[8] &= ~0x2C + chunks.append(bytes(chunk)) + cursor = end + if cursor != len(raw): + raise RosterError("avatar has trailing WebP data") + payload = b"WEBP" + b"".join(chunks) + return b"RIFF" + len(payload).to_bytes(4, "little") + payload + + def _avatar_bytes(user_id: int) -> bytes: request = urllib.request.Request( f"https://avatars.githubusercontent.com/u/{user_id}?s=128&v=4", headers={"Accept": "image/webp", "User-Agent": "apache-hugegraph-doc-community-roster/1"}, ) with urllib.request.urlopen(request, timeout=30) as response: - raw = response.read() + raw = _strip_webp_metadata(response.read()) if _webp_dimensions(raw) != (128, 128): raise RosterError(f"GitHub avatar for numeric user ID {user_id} is not 128x128 WebP") return raw @@ -247,6 +270,8 @@ def validate_bundle(warn_after_days: int) -> list[str]: raw = (ROOT / "static" / avatar.lstrip("/")).read_bytes() if hashlib.sha256(raw).hexdigest() != filename[:-5] or _webp_dimensions(raw) != (128, 128): raise RosterError(f"roster.json: invalid avatar {avatar}") + if any(marker in raw for marker in (b"EXIF", b"XMP ", b"ICCP")): + raise RosterError(f"roster.json: avatar contains metadata {avatar}") if person["profile_url"] != f"https://github.com/{expected['login']}": raise RosterError(f"roster.json: mapped profile URL mismatch") elif avatar: @@ -264,7 +289,14 @@ def validate_bundle(warn_after_days: int) -> list[str]: def validate_rendered_outputs() -> None: with tempfile.TemporaryDirectory(prefix="hugegraph-community-site-") as destination: - result = subprocess.run(["hugo", "--quiet", "--destination", destination], cwd=ROOT, text=True, capture_output=True) + environment = {**os.environ, "GOPROXY": "off"} + result = subprocess.run( + ["hugo", "--quiet", "--destination", destination], + cwd=ROOT, + env=environment, + text=True, + capture_output=True, + ) if result.returncode: raise RosterError(f"Hugo render failed:\n{result.stderr.strip()}") expected = { diff --git a/scripts/test_community_roster.py b/scripts/test_community_roster.py index e7bdbca56..e9b64de2a 100644 --- a/scripts/test_community_roster.py +++ b/scripts/test_community_roster.py @@ -35,6 +35,15 @@ class CommunityRosterTests(unittest.TestCase): with self.assertRaisesRegex(roster.RosterError, "duplicate GitHub user_id"): roster._validate_mapping(mapping, {"one", "two"}) + def test_avatar_metadata_is_stripped(self): + vp8x = b"VP8X" + (10).to_bytes(4, "little") + bytes([0x2C]) + b"\0" * 9 + exif = b"EXIF" + (4).to_bytes(4, "little") + b"meta" + payload = b"WEBP" + vp8x + exif + raw = b"RIFF" + len(payload).to_bytes(4, "little") + payload + stripped = roster._strip_webp_metadata(raw) + self.assertNotIn(b"EXIF", stripped) + self.assertEqual(0, stripped[20] & 0x2C) + def test_checked_in_bundle_validates(self): self.assertEqual([], roster.validate_bundle(90))
