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))
 

Reply via email to