From bb396bf20e66e4437dda69a9bafee0d722a9e833 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 10:54:55 +0900 Subject: [PATCH 1/6] fix(pingora): verify documentation screenshots as binary evidence Signed-off-by: Seongho Bae --- .../0019-cloudflare-pingora-edge-standard.md | 3 ++ .../pingora-documentation-image-evidence.md | 12 +++++ docs/policies/PINGORA_EDGE_POLICY.md | 6 ++- docs/product-technical-gap-baseline.md | 1 + scripts/ci/pingora_edge_policy.py | 48 ++++++++++--------- tests/test_pingora_edge_policy.py | 48 ++++++++++++++++++- 6 files changed, 92 insertions(+), 26 deletions(-) create mode 100644 docs/doctoring/pingora-documentation-image-evidence.md diff --git a/docs/adr/0019-cloudflare-pingora-edge-standard.md b/docs/adr/0019-cloudflare-pingora-edge-standard.md index 805e538b86..593160a863 100644 --- a/docs/adr/0019-cloudflare-pingora-edge-standard.md +++ b/docs/adr/0019-cloudflare-pingora-edge-standard.md @@ -33,6 +33,9 @@ so a governed shared implementation is required. 6. Initial migration does not use Pingora's experimental cache integration. 7. PHP workloads move to an HTTP application server or reviewed FastCGI adapter behind Pingora before the public listener changes. +8. Documentation PNG screenshots and PDF papers without a text diff are verified + from bounded format-magic evidence and excluded from runtime-content scanning; + runtime paths and malformed or unsupported binary evidence still fail closed. ## Consequences diff --git a/docs/doctoring/pingora-documentation-image-evidence.md b/docs/doctoring/pingora-documentation-image-evidence.md new file mode 100644 index 0000000000..58c6c7ab40 --- /dev/null +++ b/docs/doctoring/pingora-documentation-image-evidence.md @@ -0,0 +1,12 @@ +# Pingora documentation image evidence + +The required Pingora gate previously sent a changed PNG screenshot through its +UTF-8 runtime-content decoder because GitHub omits text patches for binary files. +That rejected UI evidence before the policy could determine whether it described +an active edge runtime. + +ADR-0019 now admits documentation PNG screenshots only when the bounded final +file begins with the PNG signature. Files in a runtime path, malformed signatures, +unsupported binary formats, and unavailable evidence continue to fail closed. +`tests/test_pingora_edge_policy.py` covers the accepted PNG and the existing fake +PDF/runtime cases; targeted branch coverage remains 100%. diff --git a/docs/policies/PINGORA_EDGE_POLICY.md b/docs/policies/PINGORA_EDGE_POLICY.md index 4d4c0752e1..68d1cf8537 100644 --- a/docs/policies/PINGORA_EDGE_POLICY.md +++ b/docs/policies/PINGORA_EDGE_POLICY.md @@ -53,8 +53,10 @@ The organization-required `required-workflow-bootstrap` job runs trusted base-branch scanner code at the immutable required-workflow SHA. It reads bounded changed-file metadata and final UTF-8 content through GitHub's REST API. It does not check out or execute pull-request content and receives only read permissions. -Malformed, truncated, binary, symlink, oversized, or unavailable evidence fails -closed. +Malformed, truncated, symlinked, oversized, or unavailable runtime evidence fails +closed. Documentation PNG screenshots and PDF papers without a text diff are +excluded only after bounded format-magic verification; other binary files remain +unavailable evidence and fail closed. ## Exception process diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c2ce78cf03..f0295074cf 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -93,6 +93,7 @@ flowchart LR | G-13 | hourly scheduler는 존재하지만 no-op/credential unavailable/queued Checks의 customer next action을 모든 caller가 동일한 receipt로 내는지 미확인이다 | 자동화가 실패해도 운영자가 무엇을 고쳐야 하는지 알 수 없다 | `skipped_credential_unavailable` receipt와 다음 행동 문구를 exact-head Checks로 검증하고, bounded receipt schema, retry floor, single-flight, no secret fallback을 모든 caller contract test로 고정한다 | | G-14 | release/changelog/version 증거가 각 PR에 분산되고 현재 central repo 보호 main의 release candidate가 명확하지 않다 | 운영자는 어떤 기능이 supportable release인지 확인할 수 없다 | merge 후 release readiness ledger, CHANGELOG, semantic version/tag, rollback/operability evidence를 함께 갱신한다 | | G-15 | 첨부파일 처리 경계가 제품별로 다르고, 1MB 상한은 업무 데이터와 맞지 않으며 미지원 MIME/컨테이너가 parser registry에서 명시적으로 pending/quarantine 되는지 확인되지 않았다. 현재 20MB 초과 파일 가능성과 PDF/HWP/HWPX·이미지·압축파일의 parse/sidecar 흐름을 하나의 exact contract로 묶지 못했다 | 큰 업무 첨부를 거부하거나 파싱 실패를 조용히 잃으면 고객의 메일·문서 업무가 중단된다 | naruon/newsdom-api 소유 PR에서 streaming upload, configurable bounded limit above 20MB, MIME sniffing, parser capability registry, quarantine/retry, source-position provenance, and ADR를 추가하고 size/unsupported-type/zip-bomb tests를 required evidence로 만든다 | +| G-16 | Required Pingora policy treated a changed documentation PNG screenshot as UTF-8 runtime evidence | Valid UI evidence blocked otherwise valid product PRs before policy evaluation | This branch verifies bounded PNG magic before exemption while runtime paths and malformed assets continue to fail closed; protected-main delivery remains the release gate | ## 4. 열린 PR live inventory diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index 06694fcbca..e76bfa8532 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -38,7 +38,10 @@ # 1 MiB base64 ceiling -- rejecting a legitimate research-paper citation # (this org's own "attach the relevant paper PDF" convention) for a reason # that has nothing to do with the Nginx runtime policy this module enforces. -BINARY_DOCUMENT_SUFFIXES = frozenset({".pdf"}) +BINARY_DOCUMENT_MAGIC = { + ".pdf": (b"%PDF-",), + ".png": (b"\x89PNG\r\n\x1a\n",), +} SOURCE_TEST_SUFFIXES = frozenset({".py", ".pyi", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".rs"}) LICENSE_NAMES = frozenset({"license", "license.md", "copying", "copyrights", "notice"}) DOCUMENTATION_DIRECTORIES = frozenset({"doc", "docs", "documentation"}) @@ -171,7 +174,7 @@ def _is_documentation_or_source_fixture(path: str) -> bool: """Return whether *path* is prose, license text, or scanner source fixture. Textual suffixes only: a ``.pdf`` is handled separately by - ``_is_binary_documentation_pdf`` and gated on GitHub reporting no diff + ``_is_binary_documentation_asset`` and gated on GitHub reporting no diff ``patch`` for it, so a textual file merely named with a ``.pdf`` suffix (one GitHub *can* diff, meaning it could carry inspectable content) is never exempted here. @@ -206,15 +209,15 @@ def _is_documentation_or_source_fixture(path: str) -> bool: return False -def _is_binary_documentation_pdf(changed: ChangedFile) -> bool: - """Return whether *changed* is a plausibly binary documentation PDF. +def _is_binary_documentation_asset(changed: ChangedFile) -> bool: + """Return whether *changed* is a plausibly binary documentation asset. This is only the cheap, patch-presence pre-filter: GitHub's changed-files API never returns a diff ``patch`` for a true binary file, so a missing ``patch`` is *necessary* but not *sufficient* evidence -- GitHub also omits one for a textual diff that merely exceeds its own rendering limit. A caller with network access (``evaluate_pull_request``) must - still confirm this with ``_pdf_evidence_confirms_binary`` before + still confirm this with ``_binary_documentation_evidence_confirms`` before trusting it; a caller without one (this module's own unit tests calling this function directly) is only checking the necessary condition. """ @@ -223,8 +226,9 @@ def _is_binary_documentation_pdf(changed: ChangedFile) -> bool: return False pure = PurePosixPath(changed.path) return ( - pure.suffix.lower() in BINARY_DOCUMENT_SUFFIXES + pure.suffix.lower() in BINARY_DOCUMENT_MAGIC and _is_known_documentation_path(pure) + and _runtime_path_rule(changed.path) is None ) @@ -414,10 +418,7 @@ def _load_file_content(api_url: str, repository: str, path: str, head_sha: str, raise PolicyError(f"Runtime policy candidate {path} is not valid UTF-8") from exc -_PDF_MAGIC_PREFIX = b"%PDF-" - - -def _pdf_evidence_confirms_binary( +def _binary_documentation_evidence_confirms( changed: ChangedFile, *, api_url: str, @@ -426,17 +427,18 @@ def _pdf_evidence_confirms_binary( token: str, opener: OpenJson, ) -> bool: - """Return whether a claimed binary documentation PDF is genuinely binary. + """Return whether a claimed binary documentation asset is genuine. A missing diff ``patch`` alone is not proof of binary content: GitHub also omits a patch for a textual diff that exceeds its own rendering limit, well under this module's ``MAX_FILE_BYTES`` content-fetch ceiling. Whenever the file's raw bytes can be fetched at all, this - verifies the real ``%PDF-`` magic prefix instead of trusting + verifies the declared format's magic prefix instead of trusting patch-presence alone. Only a file whose content evidently exceeds the - Contents API's size ceiling -- the exact case ``_is_binary_documentation_pdf`` + Contents API's size ceiling -- the exact case ``_is_binary_documentation_asset`` exists for, a cited, large research paper -- falls back to trusting the - path+suffix convention; every other content-evidence failure (a + path+suffix convention for oversized PDFs only; every other + content-evidence failure (a malformed API response, corrupt base64, a declared size that does not match the decoded bytes) propagates and fails the whole check closed, same as for any other file that needs scanning. @@ -445,22 +447,22 @@ def _pdf_evidence_confirms_binary( try: raw = _load_raw_file_bytes(api_url, repository, changed.path, head_sha, token, opener) except ContentSizeExceededError: - return True - return raw.startswith(_PDF_MAGIC_PREFIX) + return PurePosixPath(changed.path).suffix.lower() == ".pdf" + return raw.startswith(BINARY_DOCUMENT_MAGIC[PurePosixPath(changed.path).suffix.lower()]) def _needs_content_scan(changed: ChangedFile) -> bool: """Return whether a changed final file can carry an active edge runtime. - A claimed binary documentation PDF (``_is_binary_documentation_pdf``) + A claimed binary documentation asset (``_is_binary_documentation_asset``) exempts here on the cheap, offline pre-filter alone; ``evaluate_pull_request`` - never actually relies on that -- it runs ``_pdf_evidence_confirms_binary`` + never actually relies on that -- it runs ``_binary_documentation_evidence_confirms`` for that case before this function is even consulted. """ if changed.status == "removed" or _is_documentation_or_source_fixture(changed.path): return False - if _is_binary_documentation_pdf(changed): + if _is_binary_documentation_asset(changed): return False if not changed.patch_available: return True @@ -499,17 +501,17 @@ def evaluate_pull_request( changed_files = _load_changed_files(api_url.rstrip("/"), repository, pull_request, token, opener) violations: list[Violation] = [] for changed in changed_files: - # A claimed binary documentation PDF gets its own network-verified + # A claimed binary documentation asset gets its own network-verified # check ahead of _needs_content_scan's patch-presence-only signal: # a missing patch does not by itself prove binary content (GitHub # also omits one for an oversized textual diff), so this confirms - # the real %PDF- magic prefix whenever the bytes can be fetched at + # the format's magic prefix whenever the bytes can be fetched at # all, falling back to the path+suffix convention only when the # content genuinely exceeds the Contents API's size ceiling. A # removed file has no head content to fetch at all -- _needs_content_scan # already special-cases this the same way for every other file. - if changed.status != "removed" and _is_binary_documentation_pdf(changed): - if _pdf_evidence_confirms_binary( + if changed.status != "removed" and _is_binary_documentation_asset(changed): + if _binary_documentation_evidence_confirms( changed, api_url=api_url.rstrip("/"), repository=repository, diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index 70bb1bc970..828db6d7e0 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -383,10 +383,56 @@ def opener(url: str, _token: str) -> object: assert result == () +def test_evaluate_pull_request_exempts_a_real_documentation_png() -> None: + """A screenshot is verified by PNG magic instead of decoded as UTF-8.""" + + def opener(url: str, _token: str) -> object: + if "/pulls/15/files" in url: + return [{"filename": "docs/screenshots/dashboard.png", "status": "added"}] + assert "/contents/docs/screenshots/dashboard.png" in url + raw = b"\x89PNG\r\n\x1a\nsynthetic" + return { + "type": "file", + "encoding": "base64", + "size": len(raw), + "content": base64.b64encode(raw).decode("ascii"), + } + + assert policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=15, + head_sha="a" * 40, + event_action="opened", + token="token", + opener=opener, + ) == () + + +def test_evaluate_pull_request_rejects_a_fake_documentation_png() -> None: + """A PNG suffix without PNG magic remains runtime-content evidence.""" + + def opener(url: str, _token: str) -> object: + if "/pulls/16/files" in url: + return [{"filename": "docs/screenshots/fake.png", "status": "added"}] + return encoded_file("cat /etc/nginx/nginx.conf\n") + + result = policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=16, + head_sha="b" * 40, + event_action="opened", + token="token", + opener=opener, + ) + assert [item.rule for item in result] == ["nginx_runtime_path"] + + def test_evaluate_pull_request_does_not_fetch_a_removed_binary_pdf() -> None: """A removed documentation PDF has no head content to fetch at all. - Regression coverage for Devin Review's finding: _is_binary_documentation_pdf + Regression coverage for Devin Review's finding: _is_binary_documentation_asset does not itself check status, so without an explicit removed-status guard in evaluate_pull_request's own loop, a deleted PDF would try to fetch its (nonexistent) head content and fail evidence collection for every such From 48922bcc8ad95fa69273f5c53be496d7d00d0401 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 11:01:47 +0900 Subject: [PATCH 2/6] fix(pingora): validate complete PNG evidence Signed-off-by: Seongho Bae --- .../0019-cloudflare-pingora-edge-standard.md | 3 +- .../pingora-documentation-image-evidence.md | 3 +- docs/policies/PINGORA_EDGE_POLICY.md | 3 +- scripts/ci/pingora_edge_policy.py | 37 ++++++++++++- tests/test_pingora_edge_policy.py | 53 ++++++++++++++++++- 5 files changed, 94 insertions(+), 5 deletions(-) diff --git a/docs/adr/0019-cloudflare-pingora-edge-standard.md b/docs/adr/0019-cloudflare-pingora-edge-standard.md index 593160a863..7e6264c885 100644 --- a/docs/adr/0019-cloudflare-pingora-edge-standard.md +++ b/docs/adr/0019-cloudflare-pingora-edge-standard.md @@ -34,7 +34,8 @@ so a governed shared implementation is required. 7. PHP workloads move to an HTTP application server or reviewed FastCGI adapter behind Pingora before the public listener changes. 8. Documentation PNG screenshots and PDF papers without a text diff are verified - from bounded format-magic evidence and excluded from runtime-content scanning; + from bounded format evidence (a complete CRC-valid PNG chunk stream or PDF + signature) and excluded from runtime-content scanning; runtime paths and malformed or unsupported binary evidence still fail closed. ## Consequences diff --git a/docs/doctoring/pingora-documentation-image-evidence.md b/docs/doctoring/pingora-documentation-image-evidence.md index 58c6c7ab40..364132ddcf 100644 --- a/docs/doctoring/pingora-documentation-image-evidence.md +++ b/docs/doctoring/pingora-documentation-image-evidence.md @@ -6,7 +6,8 @@ That rejected UI evidence before the policy could determine whether it described an active edge runtime. ADR-0019 now admits documentation PNG screenshots only when the bounded final -file begins with the PNG signature. Files in a runtime path, malformed signatures, +file is a complete CRC-valid PNG chunk stream ending at IEND with no trailing +payload. Files in a runtime path, malformed signatures, unsupported binary formats, and unavailable evidence continue to fail closed. `tests/test_pingora_edge_policy.py` covers the accepted PNG and the existing fake PDF/runtime cases; targeted branch coverage remains 100%. diff --git a/docs/policies/PINGORA_EDGE_POLICY.md b/docs/policies/PINGORA_EDGE_POLICY.md index 68d1cf8537..bc37c9f33e 100644 --- a/docs/policies/PINGORA_EDGE_POLICY.md +++ b/docs/policies/PINGORA_EDGE_POLICY.md @@ -55,7 +55,8 @@ changed-file metadata and final UTF-8 content through GitHub's REST API. It does not check out or execute pull-request content and receives only read permissions. Malformed, truncated, symlinked, oversized, or unavailable runtime evidence fails closed. Documentation PNG screenshots and PDF papers without a text diff are -excluded only after bounded format-magic verification; other binary files remain +excluded only after bounded format verification; PNG evidence must be a complete +CRC-valid chunk stream ending at IEND. Other binary files remain unavailable evidence and fail closed. ## Exception process diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index e76bfa8532..eff773af97 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -15,6 +15,7 @@ import os import re import sys +import zlib from dataclasses import dataclass from pathlib import PurePosixPath from typing import Callable, Mapping, Sequence @@ -42,6 +43,7 @@ ".pdf": (b"%PDF-",), ".png": (b"\x89PNG\r\n\x1a\n",), } +PNG_SIGNATURE = BINARY_DOCUMENT_MAGIC[".png"][0] SOURCE_TEST_SUFFIXES = frozenset({".py", ".pyi", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".rs"}) LICENSE_NAMES = frozenset({"license", "license.md", "copying", "copyrights", "notice"}) DOCUMENTATION_DIRECTORIES = frozenset({"doc", "docs", "documentation"}) @@ -448,7 +450,40 @@ def _binary_documentation_evidence_confirms( raw = _load_raw_file_bytes(api_url, repository, changed.path, head_sha, token, opener) except ContentSizeExceededError: return PurePosixPath(changed.path).suffix.lower() == ".pdf" - return raw.startswith(BINARY_DOCUMENT_MAGIC[PurePosixPath(changed.path).suffix.lower()]) + suffix = PurePosixPath(changed.path).suffix.lower() + if suffix == ".png": + return _is_complete_png(raw) + return raw.startswith(BINARY_DOCUMENT_MAGIC[suffix]) + + +def _is_complete_png(raw: bytes) -> bool: + """Validate a complete PNG chunk stream with no appended payload.""" + + if not raw.startswith(PNG_SIGNATURE): + return False + offset = len(PNG_SIGNATURE) + saw_header = False + saw_image_data = False + while offset + 12 <= len(raw): + length = int.from_bytes(raw[offset : offset + 4], "big") + chunk_end = offset + 12 + length + if chunk_end > len(raw): + return False + chunk_type = raw[offset + 4 : offset + 8] + chunk_data = raw[offset + 8 : offset + 8 + length] + expected_crc = int.from_bytes(raw[offset + 8 + length : chunk_end], "big") + if zlib.crc32(chunk_type + chunk_data) != expected_crc: + return False + if not saw_header: + if chunk_type != b"IHDR" or length != 13: + return False + saw_header = True + elif chunk_type == b"IDAT": + saw_image_data = True + elif chunk_type == b"IEND": + return length == 0 and saw_image_data and chunk_end == len(raw) + offset = chunk_end + return False def _needs_content_scan(changed: ChangedFile) -> bool: diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index 828db6d7e0..2a0271b2f5 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -7,6 +7,7 @@ import inspect import re import sys +import zlib from io import BytesIO from pathlib import Path from urllib.error import HTTPError, URLError @@ -390,7 +391,9 @@ def opener(url: str, _token: str) -> object: if "/pulls/15/files" in url: return [{"filename": "docs/screenshots/dashboard.png", "status": "added"}] assert "/contents/docs/screenshots/dashboard.png" in url - raw = b"\x89PNG\r\n\x1a\nsynthetic" + raw = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" + ) return { "type": "file", "encoding": "base64", @@ -429,6 +432,54 @@ def opener(url: str, _token: str) -> object: assert [item.rule for item in result] == ["nginx_runtime_path"] +def test_evaluate_pull_request_rejects_png_with_appended_runtime_text() -> None: + """A valid image prefix cannot hide bytes appended after the IEND chunk.""" + + image = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" + ) + + def opener(url: str, _token: str) -> object: + if "/pulls/17/files" in url: + return [{"filename": "docs/screenshots/forged.png", "status": "added"}] + raw = image + b"\ncat /etc/nginx/nginx.conf\n" + return { + "type": "file", "encoding": "base64", "size": len(raw), + "content": base64.b64encode(raw).decode("ascii"), + } + + with pytest.raises(policy.PolicyError, match="not valid UTF-8"): + policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=17, + head_sha="c" * 40, + event_action="opened", + token="token", + opener=opener, + ) + + +def test_png_structure_validation_fails_closed_on_malformed_chunks() -> None: + """Every malformed PNG boundary returns false without parsing past bounds.""" + + def chunk(kind: bytes, data: bytes) -> bytes: + payload = kind + data + return len(data).to_bytes(4, "big") + payload + zlib.crc32(payload).to_bytes(4, "big") + + signature = policy.PNG_SIGNATURE + header = chunk(b"IHDR", b"\0" * 13) + assert not policy._is_complete_png(b"not-png") + assert not policy._is_complete_png(signature) + assert not policy._is_complete_png( + signature + (99).to_bytes(4, "big") + b"IHDR" + b"\0" * 4 + ) + assert not policy._is_complete_png(signature + header[:-1] + b"\0") + assert not policy._is_complete_png(signature + chunk(b"TEXT", b"")) + assert not policy._is_complete_png(signature + header + chunk(b"IEND", b"")) + assert not policy._is_complete_png(signature + header + chunk(b"TEXT", b"")) + + def test_evaluate_pull_request_does_not_fetch_a_removed_binary_pdf() -> None: """A removed documentation PDF has no head content to fetch at all. From d52accec48064d4003778a7521da787f928c23dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 11:11:11 +0900 Subject: [PATCH 3/6] fix(ci): validate PNG image streams Signed-off-by: Seongho Bae --- .../0019-cloudflare-pingora-edge-standard.md | 5 +- .../pingora-documentation-image-evidence.md | 3 +- docs/policies/PINGORA_EDGE_POLICY.md | 3 +- scripts/ci/pingora_edge_policy.py | 65 ++++++++++++++++--- tests/test_pingora_edge_policy.py | 41 ++++++++++++ 5 files changed, 105 insertions(+), 12 deletions(-) diff --git a/docs/adr/0019-cloudflare-pingora-edge-standard.md b/docs/adr/0019-cloudflare-pingora-edge-standard.md index 7e6264c885..4d5f4232eb 100644 --- a/docs/adr/0019-cloudflare-pingora-edge-standard.md +++ b/docs/adr/0019-cloudflare-pingora-edge-standard.md @@ -34,8 +34,9 @@ so a governed shared implementation is required. 7. PHP workloads move to an HTTP application server or reviewed FastCGI adapter behind Pingora before the public listener changes. 8. Documentation PNG screenshots and PDF papers without a text diff are verified - from bounded format evidence (a complete CRC-valid PNG chunk stream or PDF - signature) and excluded from runtime-content scanning; + from bounded format evidence (a complete CRC-valid PNG chunk stream whose + bounded decompressed scanlines match IHDR, or a PDF signature) and excluded + from runtime-content scanning; runtime paths and malformed or unsupported binary evidence still fail closed. ## Consequences diff --git a/docs/doctoring/pingora-documentation-image-evidence.md b/docs/doctoring/pingora-documentation-image-evidence.md index 364132ddcf..3c4eafbefa 100644 --- a/docs/doctoring/pingora-documentation-image-evidence.md +++ b/docs/doctoring/pingora-documentation-image-evidence.md @@ -7,7 +7,8 @@ an active edge runtime. ADR-0019 now admits documentation PNG screenshots only when the bounded final file is a complete CRC-valid PNG chunk stream ending at IEND with no trailing -payload. Files in a runtime path, malformed signatures, +payload, and its bounded decompressed scanlines must match IHDR. A signature or +CRC-valid arbitrary IDAT is insufficient. Files in a runtime path, malformed signatures, unsupported binary formats, and unavailable evidence continue to fail closed. `tests/test_pingora_edge_policy.py` covers the accepted PNG and the existing fake PDF/runtime cases; targeted branch coverage remains 100%. diff --git a/docs/policies/PINGORA_EDGE_POLICY.md b/docs/policies/PINGORA_EDGE_POLICY.md index bc37c9f33e..695da53444 100644 --- a/docs/policies/PINGORA_EDGE_POLICY.md +++ b/docs/policies/PINGORA_EDGE_POLICY.md @@ -56,7 +56,8 @@ not check out or execute pull-request content and receives only read permissions Malformed, truncated, symlinked, oversized, or unavailable runtime evidence fails closed. Documentation PNG screenshots and PDF papers without a text diff are excluded only after bounded format verification; PNG evidence must be a complete -CRC-valid chunk stream ending at IEND. Other binary files remain +CRC-valid chunk stream ending at IEND whose bounded decompressed scanlines match +IHDR. Other binary files remain unavailable evidence and fail closed. ## Exception process diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index eff773af97..ba15b2725c 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -457,13 +457,15 @@ def _binary_documentation_evidence_confirms( def _is_complete_png(raw: bytes) -> bool: - """Validate a complete PNG chunk stream with no appended payload.""" + """Validate one bounded, non-interlaced PNG including its image stream.""" if not raw.startswith(PNG_SIGNATURE): return False offset = len(PNG_SIGNATURE) - saw_header = False - saw_image_data = False + header: tuple[int, int, int, int] | None = None + palette_seen = False + image_data: list[bytes] = [] + image_data_closed = False while offset + 12 <= len(raw): length = int.from_bytes(raw[offset : offset + 4], "big") chunk_end = offset + 12 + length @@ -474,14 +476,61 @@ def _is_complete_png(raw: bytes) -> bool: expected_crc = int.from_bytes(raw[offset + 8 + length : chunk_end], "big") if zlib.crc32(chunk_type + chunk_data) != expected_crc: return False - if not saw_header: - if chunk_type != b"IHDR" or length != 13: + if header is None: + if chunk_type != b"IHDR" or length != 13 or offset != len(PNG_SIGNATURE): return False - saw_header = True + width = int.from_bytes(chunk_data[0:4], "big") + height = int.from_bytes(chunk_data[4:8], "big") + bit_depth, color_type, compression, filtering, interlace = chunk_data[8:13] + allowed_depths = { + 0: {1, 2, 4, 8, 16}, 2: {8, 16}, 3: {1, 2, 4, 8}, + 4: {8, 16}, 6: {8, 16}, + } + if ( + width == 0 or height == 0 + or bit_depth not in allowed_depths.get(color_type, set()) + or compression != 0 or filtering != 0 or interlace != 0 + ): + return False + header = (width, height, bit_depth, color_type) + elif chunk_type == b"IHDR": + return False + elif chunk_type == b"PLTE": + if palette_seen or image_data or length == 0 or length > 768 or length % 3: + return False + palette_seen = True elif chunk_type == b"IDAT": - saw_image_data = True + if image_data_closed: + return False + image_data.append(chunk_data) elif chunk_type == b"IEND": - return length == 0 and saw_image_data and chunk_end == len(raw) + if length != 0 or not image_data or chunk_end != len(raw): + return False + width, height, bit_depth, color_type = header + if (color_type == 3 and not palette_seen) or ( + color_type in {0, 4} and palette_seen + ): + return False + channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[color_type] + row_bytes = (width * channels * bit_depth + 7) // 8 + expected_size = height * (row_bytes + 1) + if expected_size > MAX_RESPONSE_BYTES: + return False + decoder = zlib.decompressobj() + try: + decoded = decoder.decompress(b"".join(image_data), expected_size + 1) + except zlib.error: + return False + if ( + len(decoded) != expected_size or not decoder.eof + or decoder.unused_data or decoder.unconsumed_tail + ): + return False + return all(decoded[row * (row_bytes + 1)] <= 4 for row in range(height)) + elif chunk_type[0] & 0x20 == 0: + return False + elif image_data: + image_data_closed = True offset = chunk_end return False diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index 2a0271b2f5..f50857700e 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -480,6 +480,47 @@ def chunk(kind: bytes, data: bytes) -> bytes: assert not policy._is_complete_png(signature + header + chunk(b"TEXT", b"")) +def test_png_semantic_validation_fails_closed() -> None: + """CRC-valid chunks still need a valid bounded PNG image stream.""" + + def chunk(kind: bytes, data: bytes) -> bytes: + payload = kind + data + return len(data).to_bytes(4, "big") + payload + zlib.crc32(payload).to_bytes(4, "big") + + def png(header: bytes, *chunks: bytes) -> bytes: + return policy.PNG_SIGNATURE + chunk(b"IHDR", header) + b"".join(chunks) + + rgba = (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 0)) + indexed = (1).to_bytes(4, "big") * 2 + bytes((8, 3, 0, 0, 0)) + gray = (1).to_bytes(4, "big") * 2 + bytes((8, 0, 0, 0, 0)) + image = chunk(b"IDAT", zlib.compress(b"\0\0\0\0\0")) + end = chunk(b"IEND", b"") + + invalid_headers = ( + b"\0" * 13, + (1).to_bytes(4, "big") * 2 + bytes((4, 2, 0, 0, 0)), + (1).to_bytes(4, "big") * 2 + bytes((8, 6, 1, 0, 0)), + (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 1, 0)), + (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 1)), + ) + assert all(not policy._is_complete_png(png(header, image, end)) for header in invalid_headers) + assert not policy._is_complete_png(png(rgba, chunk(b"IHDR", rgba), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"PLTE", b""), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"PLTE", b"x" * 769), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"PLTE", b"x"), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"ABCD", b""), image, end)) + assert policy._is_complete_png(png(rgba, chunk(b"tEXt", b"x"), image, end)) + assert not policy._is_complete_png(png(rgba, image, chunk(b"tEXt", b"x"), image, end)) + assert not policy._is_complete_png(png(indexed, image, end)) + assert not policy._is_complete_png(png(gray, chunk(b"PLTE", b"\0\0\0"), chunk(b"IDAT", zlib.compress(b"\0\0")), end)) + assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", b"not-zlib"), end)) + assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\0")), end)) + assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\0\0\0\0\0") + b"x"), end)) + assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\5\0\0\0\0")), end)) + huge = (policy.MAX_RESPONSE_BYTES).to_bytes(4, "big") + (1).to_bytes(4, "big") + bytes((8, 6, 0, 0, 0)) + assert not policy._is_complete_png(png(huge, image, end)) + + def test_evaluate_pull_request_does_not_fetch_a_removed_binary_pdf() -> None: """A removed documentation PDF has no head content to fetch at all. From 791441c382ea86d5ec997c209a187761374d7549 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 15:34:36 +0900 Subject: [PATCH 4/6] fix(pingora): validate conforming PNG evidence Signed-off-by: Seongho Bae --- .../0019-cloudflare-pingora-edge-standard.md | 5 +- .../pingora-documentation-image-evidence.md | 5 +- docs/policies/PINGORA_EDGE_POLICY.md | 8 ++-- scripts/ci/pingora_edge_policy.py | 48 ++++++++++++++----- tests/test_pingora_edge_policy.py | 22 ++++++++- 5 files changed, 70 insertions(+), 18 deletions(-) diff --git a/docs/adr/0019-cloudflare-pingora-edge-standard.md b/docs/adr/0019-cloudflare-pingora-edge-standard.md index 4d5f4232eb..04d0cc968d 100644 --- a/docs/adr/0019-cloudflare-pingora-edge-standard.md +++ b/docs/adr/0019-cloudflare-pingora-edge-standard.md @@ -34,8 +34,9 @@ so a governed shared implementation is required. 7. PHP workloads move to an HTTP application server or reviewed FastCGI adapter behind Pingora before the public listener changes. 8. Documentation PNG screenshots and PDF papers without a text diff are verified - from bounded format evidence (a complete CRC-valid PNG chunk stream whose - bounded decompressed scanlines match IHDR, or a PDF signature) and excluded + from bounded format evidence (a complete CRC-valid PNG chunk stream with + conforming chunk names and palette bounds whose bounded null- or + Adam7-interlaced decompressed scanlines match IHDR, or a PDF signature) and excluded from runtime-content scanning; runtime paths and malformed or unsupported binary evidence still fail closed. diff --git a/docs/doctoring/pingora-documentation-image-evidence.md b/docs/doctoring/pingora-documentation-image-evidence.md index 3c4eafbefa..fb64ebb7d7 100644 --- a/docs/doctoring/pingora-documentation-image-evidence.md +++ b/docs/doctoring/pingora-documentation-image-evidence.md @@ -7,8 +7,11 @@ an active edge runtime. ADR-0019 now admits documentation PNG screenshots only when the bounded final file is a complete CRC-valid PNG chunk stream ending at IEND with no trailing -payload, and its bounded decompressed scanlines must match IHDR. A signature or +payload, conforming chunk names and palette bounds, and bounded null- or +Adam7-interlaced decompressed scanlines that match IHDR. A signature or CRC-valid arbitrary IDAT is insufficient. Files in a runtime path, malformed signatures, unsupported binary formats, and unavailable evidence continue to fail closed. +The gate establishes bounded binary evidence rather than general image-rendering +fidelity; optional ancillary-chunk semantics are outside this policy boundary. `tests/test_pingora_edge_policy.py` covers the accepted PNG and the existing fake PDF/runtime cases; targeted branch coverage remains 100%. diff --git a/docs/policies/PINGORA_EDGE_POLICY.md b/docs/policies/PINGORA_EDGE_POLICY.md index 695da53444..4c1f95b4bb 100644 --- a/docs/policies/PINGORA_EDGE_POLICY.md +++ b/docs/policies/PINGORA_EDGE_POLICY.md @@ -56,9 +56,11 @@ not check out or execute pull-request content and receives only read permissions Malformed, truncated, symlinked, oversized, or unavailable runtime evidence fails closed. Documentation PNG screenshots and PDF papers without a text diff are excluded only after bounded format verification; PNG evidence must be a complete -CRC-valid chunk stream ending at IEND whose bounded decompressed scanlines match -IHDR. Other binary files remain -unavailable evidence and fail closed. +CRC-valid chunk stream ending at IEND with conforming chunk names and palette +bounds whose bounded null- or Adam7-interlaced decompressed scanlines match IHDR. +This is a bounded binary-evidence classifier, not a general image renderer; +visual fidelity and optional ancillary-chunk semantics are outside this gate. +Other binary files remain unavailable evidence and fail closed. ## Exception process diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index ba15b2725c..d089f5cee2 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -457,12 +457,12 @@ def _binary_documentation_evidence_confirms( def _is_complete_png(raw: bytes) -> bool: - """Validate one bounded, non-interlaced PNG including its image stream.""" + """Validate one bounded PNG including its null- or Adam7-interlaced stream.""" if not raw.startswith(PNG_SIGNATURE): return False offset = len(PNG_SIGNATURE) - header: tuple[int, int, int, int] | None = None + header: tuple[int, int, int, int, int] | None = None palette_seen = False image_data: list[bytes] = [] image_data_closed = False @@ -474,7 +474,11 @@ def _is_complete_png(raw: bytes) -> bool: chunk_type = raw[offset + 4 : offset + 8] chunk_data = raw[offset + 8 : offset + 8 + length] expected_crc = int.from_bytes(raw[offset + 8 + length : chunk_end], "big") - if zlib.crc32(chunk_type + chunk_data) != expected_crc: + if ( + any(not (65 <= byte <= 90 or 97 <= byte <= 122) for byte in chunk_type) + or chunk_type[2] & 0x20 + or zlib.crc32(chunk_type + chunk_data) != expected_crc + ): return False if header is None: if chunk_type != b"IHDR" or length != 13 or offset != len(PNG_SIGNATURE): @@ -489,15 +493,18 @@ def _is_complete_png(raw: bytes) -> bool: if ( width == 0 or height == 0 or bit_depth not in allowed_depths.get(color_type, set()) - or compression != 0 or filtering != 0 or interlace != 0 + or compression != 0 or filtering != 0 or interlace not in {0, 1} ): return False - header = (width, height, bit_depth, color_type) + header = (width, height, bit_depth, color_type, interlace) elif chunk_type == b"IHDR": return False elif chunk_type == b"PLTE": if palette_seen or image_data or length == 0 or length > 768 or length % 3: return False + _width, _height, bit_depth, color_type, _interlace = header + if color_type == 3 and length // 3 > 1 << bit_depth: + return False palette_seen = True elif chunk_type == b"IDAT": if image_data_closed: @@ -506,16 +513,29 @@ def _is_complete_png(raw: bytes) -> bool: elif chunk_type == b"IEND": if length != 0 or not image_data or chunk_end != len(raw): return False - width, height, bit_depth, color_type = header + width, height, bit_depth, color_type, interlace = header if (color_type == 3 and not palette_seen) or ( color_type in {0, 4} and palette_seen ): return False channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[color_type] - row_bytes = (width * channels * bit_depth + 7) // 8 - expected_size = height * (row_bytes + 1) - if expected_size > MAX_RESPONSE_BYTES: - return False + passes = ( + ((0, 0, 8, 8), (4, 0, 8, 8), (0, 4, 4, 8), (2, 0, 4, 4), + (0, 2, 2, 4), (1, 0, 2, 2), (0, 1, 1, 2)) + if interlace else ((0, 0, 1, 1),) + ) + scanlines: list[tuple[int, int]] = [] + expected_size = 0 + for x_start, y_start, x_step, y_step in passes: + if width <= x_start or height <= y_start: + continue + pass_width = (width - x_start + x_step - 1) // x_step + pass_height = (height - y_start + y_step - 1) // y_step + row_bytes = (pass_width * channels * bit_depth + 7) // 8 + expected_size += pass_height * (row_bytes + 1) + if expected_size > MAX_RESPONSE_BYTES: + return False + scanlines.append((pass_height, row_bytes)) decoder = zlib.decompressobj() try: decoded = decoder.decompress(b"".join(image_data), expected_size + 1) @@ -526,7 +546,13 @@ def _is_complete_png(raw: bytes) -> bool: or decoder.unused_data or decoder.unconsumed_tail ): return False - return all(decoded[row * (row_bytes + 1)] <= 4 for row in range(height)) + decoded_offset = 0 + for row_count, row_bytes in scanlines: + for _ in range(row_count): + if decoded[decoded_offset] > 4: + return False + decoded_offset += row_bytes + 1 + return decoded_offset == len(decoded) elif chunk_type[0] & 0x20 == 0: return False elif image_data: diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index f50857700e..136c6ed00b 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -501,17 +501,23 @@ def png(header: bytes, *chunks: bytes) -> bytes: (1).to_bytes(4, "big") * 2 + bytes((4, 2, 0, 0, 0)), (1).to_bytes(4, "big") * 2 + bytes((8, 6, 1, 0, 0)), (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 1, 0)), - (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 1)), + (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 2)), ) assert all(not policy._is_complete_png(png(header, image, end)) for header in invalid_headers) assert not policy._is_complete_png(png(rgba, chunk(b"IHDR", rgba), image, end)) assert not policy._is_complete_png(png(rgba, chunk(b"PLTE", b""), image, end)) assert not policy._is_complete_png(png(rgba, chunk(b"PLTE", b"x" * 769), image, end)) assert not policy._is_complete_png(png(rgba, chunk(b"PLTE", b"x"), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"1EXt", b""), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"tExt", b""), image, end)) assert not policy._is_complete_png(png(rgba, chunk(b"ABCD", b""), image, end)) assert policy._is_complete_png(png(rgba, chunk(b"tEXt", b"x"), image, end)) assert not policy._is_complete_png(png(rgba, image, chunk(b"tEXt", b"x"), image, end)) assert not policy._is_complete_png(png(indexed, image, end)) + indexed_one_bit = (1).to_bytes(4, "big") * 2 + bytes((1, 3, 0, 0, 0)) + assert not policy._is_complete_png( + png(indexed_one_bit, chunk(b"PLTE", b"\0" * 9), chunk(b"IDAT", zlib.compress(b"\0\0")), end) + ) assert not policy._is_complete_png(png(gray, chunk(b"PLTE", b"\0\0\0"), chunk(b"IDAT", zlib.compress(b"\0\0")), end)) assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", b"not-zlib"), end)) assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\0")), end)) @@ -520,6 +526,20 @@ def png(header: bytes, *chunks: bytes) -> bytes: huge = (policy.MAX_RESPONSE_BYTES).to_bytes(4, "big") + (1).to_bytes(4, "big") + bytes((8, 6, 0, 0, 0)) assert not policy._is_complete_png(png(huge, image, end)) + adam7 = (8).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 1)) + adam7_scanlines = b"".join( + b"\0" + b"\0" * (pass_width * 4) + for pass_width, pass_height in ((1, 1), (1, 1), (2, 1), (2, 2), (4, 2), (4, 4), (8, 4)) + for _ in range(pass_height) + ) + assert policy._is_complete_png( + png(adam7, chunk(b"IDAT", zlib.compress(adam7_scanlines)), end) + ) + adam7_one_pixel = (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 1)) + assert policy._is_complete_png( + png(adam7_one_pixel, chunk(b"IDAT", zlib.compress(b"\0\0\0\0\0")), end) + ) + def test_evaluate_pull_request_does_not_fetch_a_removed_binary_pdf() -> None: """A removed documentation PDF has no head content to fetch at all. From 16bbe7d76c38e3e56f8cadf089759c8369fee27e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 15:39:44 +0900 Subject: [PATCH 5/6] fix(pingora): reject invalid palette indices Signed-off-by: Seongho Bae --- .../0019-cloudflare-pingora-edge-standard.md | 2 +- .../pingora-documentation-image-evidence.md | 2 +- docs/policies/PINGORA_EDGE_POLICY.md | 5 +- scripts/ci/pingora_edge_policy.py | 57 ++++++++++++++++--- tests/test_pingora_edge_policy.py | 28 +++++++++ 5 files changed, 81 insertions(+), 13 deletions(-) diff --git a/docs/adr/0019-cloudflare-pingora-edge-standard.md b/docs/adr/0019-cloudflare-pingora-edge-standard.md index 04d0cc968d..9f92f0f046 100644 --- a/docs/adr/0019-cloudflare-pingora-edge-standard.md +++ b/docs/adr/0019-cloudflare-pingora-edge-standard.md @@ -35,7 +35,7 @@ so a governed shared implementation is required. behind Pingora before the public listener changes. 8. Documentation PNG screenshots and PDF papers without a text diff are verified from bounded format evidence (a complete CRC-valid PNG chunk stream with - conforming chunk names and palette bounds whose bounded null- or + conforming chunk names, palette bounds, and palette indices whose bounded null- or Adam7-interlaced decompressed scanlines match IHDR, or a PDF signature) and excluded from runtime-content scanning; runtime paths and malformed or unsupported binary evidence still fail closed. diff --git a/docs/doctoring/pingora-documentation-image-evidence.md b/docs/doctoring/pingora-documentation-image-evidence.md index fb64ebb7d7..af10942cd8 100644 --- a/docs/doctoring/pingora-documentation-image-evidence.md +++ b/docs/doctoring/pingora-documentation-image-evidence.md @@ -7,7 +7,7 @@ an active edge runtime. ADR-0019 now admits documentation PNG screenshots only when the bounded final file is a complete CRC-valid PNG chunk stream ending at IEND with no trailing -payload, conforming chunk names and palette bounds, and bounded null- or +payload, conforming chunk names, palette bounds and indices, and bounded null- or Adam7-interlaced decompressed scanlines that match IHDR. A signature or CRC-valid arbitrary IDAT is insufficient. Files in a runtime path, malformed signatures, unsupported binary formats, and unavailable evidence continue to fail closed. diff --git a/docs/policies/PINGORA_EDGE_POLICY.md b/docs/policies/PINGORA_EDGE_POLICY.md index 4c1f95b4bb..619374a13d 100644 --- a/docs/policies/PINGORA_EDGE_POLICY.md +++ b/docs/policies/PINGORA_EDGE_POLICY.md @@ -56,8 +56,9 @@ not check out or execute pull-request content and receives only read permissions Malformed, truncated, symlinked, oversized, or unavailable runtime evidence fails closed. Documentation PNG screenshots and PDF papers without a text diff are excluded only after bounded format verification; PNG evidence must be a complete -CRC-valid chunk stream ending at IEND with conforming chunk names and palette -bounds whose bounded null- or Adam7-interlaced decompressed scanlines match IHDR. +CRC-valid chunk stream ending at IEND with conforming chunk names, palette +bounds, and palette indices whose bounded null- or Adam7-interlaced decompressed +scanlines match IHDR. This is a bounded binary-evidence classifier, not a general image renderer; visual fidelity and optional ancillary-chunk semantics are outside this gate. Other binary files remain unavailable evidence and fail closed. diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index d089f5cee2..515767f045 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -456,6 +456,30 @@ def _binary_documentation_evidence_confirms( return raw.startswith(BINARY_DOCUMENT_MAGIC[suffix]) +def _png_unfilter_row(filtered: bytes, previous: bytes, filter_type: int, bytes_per_pixel: int) -> bytes: + """Reconstruct one PNG scanline for bounded indexed-pixel validation.""" + + reconstructed = bytearray(len(filtered)) + for index, value in enumerate(filtered): + left = reconstructed[index - bytes_per_pixel] if index >= bytes_per_pixel else 0 + above = previous[index] if previous else 0 + upper_left = previous[index - bytes_per_pixel] if previous and index >= bytes_per_pixel else 0 + if filter_type == 0: + predictor = 0 + elif filter_type == 1: + predictor = left + elif filter_type == 2: + predictor = above + elif filter_type == 3: + predictor = (left + above) // 2 + else: + estimate = left + above - upper_left + distances = (abs(estimate - left), abs(estimate - above), abs(estimate - upper_left)) + predictor = (left, above, upper_left)[distances.index(min(distances))] + reconstructed[index] = (value + predictor) & 0xFF + return bytes(reconstructed) + + def _is_complete_png(raw: bytes) -> bool: """Validate one bounded PNG including its null- or Adam7-interlaced stream.""" @@ -463,7 +487,7 @@ def _is_complete_png(raw: bytes) -> bool: return False offset = len(PNG_SIGNATURE) header: tuple[int, int, int, int, int] | None = None - palette_seen = False + palette_entries = 0 image_data: list[bytes] = [] image_data_closed = False while offset + 12 <= len(raw): @@ -500,12 +524,12 @@ def _is_complete_png(raw: bytes) -> bool: elif chunk_type == b"IHDR": return False elif chunk_type == b"PLTE": - if palette_seen or image_data or length == 0 or length > 768 or length % 3: + if palette_entries or image_data or length == 0 or length > 768 or length % 3: return False _width, _height, bit_depth, color_type, _interlace = header if color_type == 3 and length // 3 > 1 << bit_depth: return False - palette_seen = True + palette_entries = length // 3 elif chunk_type == b"IDAT": if image_data_closed: return False @@ -514,8 +538,8 @@ def _is_complete_png(raw: bytes) -> bool: if length != 0 or not image_data or chunk_end != len(raw): return False width, height, bit_depth, color_type, interlace = header - if (color_type == 3 and not palette_seen) or ( - color_type in {0, 4} and palette_seen + if (color_type == 3 and not palette_entries) or ( + color_type in {0, 4} and palette_entries ): return False channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[color_type] @@ -524,7 +548,7 @@ def _is_complete_png(raw: bytes) -> bool: (0, 2, 2, 4), (1, 0, 2, 2), (0, 1, 1, 2)) if interlace else ((0, 0, 1, 1),) ) - scanlines: list[tuple[int, int]] = [] + scanlines: list[tuple[int, int, int]] = [] expected_size = 0 for x_start, y_start, x_step, y_step in passes: if width <= x_start or height <= y_start: @@ -535,7 +559,7 @@ def _is_complete_png(raw: bytes) -> bool: expected_size += pass_height * (row_bytes + 1) if expected_size > MAX_RESPONSE_BYTES: return False - scanlines.append((pass_height, row_bytes)) + scanlines.append((pass_height, row_bytes, pass_width)) decoder = zlib.decompressobj() try: decoded = decoder.decompress(b"".join(image_data), expected_size + 1) @@ -547,10 +571,25 @@ def _is_complete_png(raw: bytes) -> bool: ): return False decoded_offset = 0 - for row_count, row_bytes in scanlines: + for row_count, row_bytes, pass_width in scanlines: + previous = b"" for _ in range(row_count): - if decoded[decoded_offset] > 4: + filter_type = decoded[decoded_offset] + if filter_type > 4: return False + filtered = decoded[decoded_offset + 1 : decoded_offset + row_bytes + 1] + if color_type == 3: + reconstructed = _png_unfilter_row(filtered, previous, filter_type, 1) + mask = (1 << bit_depth) - 1 + for pixel in range(pass_width): + bit_offset = pixel * bit_depth + palette_index = ( + reconstructed[bit_offset // 8] + >> (8 - bit_depth - bit_offset % 8) + ) & mask + if palette_index >= palette_entries: + return False + previous = reconstructed decoded_offset += row_bytes + 1 return decoded_offset == len(decoded) elif chunk_type[0] & 0x20 == 0: diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index 136c6ed00b..e5d88cb972 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -490,6 +490,23 @@ def chunk(kind: bytes, data: bytes) -> bytes: def png(header: bytes, *chunks: bytes) -> bytes: return policy.PNG_SIGNATURE + chunk(b"IHDR", header) + b"".join(chunks) + def indexed_png( + width: int, + height: int, + bit_depth: int, + palette_entries: int, + decoded: bytes, + *, + interlace: int = 0, + ) -> bytes: + header = width.to_bytes(4, "big") + height.to_bytes(4, "big") + bytes((bit_depth, 3, 0, 0, interlace)) + return png( + header, + chunk(b"PLTE", b"\0\0\0" * palette_entries), + chunk(b"IDAT", zlib.compress(decoded)), + chunk(b"IEND", b""), + ) + rgba = (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 0)) indexed = (1).to_bytes(4, "big") * 2 + bytes((8, 3, 0, 0, 0)) gray = (1).to_bytes(4, "big") * 2 + bytes((8, 0, 0, 0, 0)) @@ -518,6 +535,17 @@ def png(header: bytes, *chunks: bytes) -> bytes: assert not policy._is_complete_png( png(indexed_one_bit, chunk(b"PLTE", b"\0" * 9), chunk(b"IDAT", zlib.compress(b"\0\0")), end) ) + for filter_type in range(5): + second_row = b"\1\0" if filter_type == 0 else b"\1\xff" + assert policy._is_complete_png( + indexed_png(2, 2, 8, 2, bytes((filter_type, 0, 1, filter_type)) + second_row) + ) + assert not policy._is_complete_png(indexed_png(2, 1, 8, 1, b"\0\0\1")) + assert not policy._is_complete_png(indexed_png(2, 2, 8, 2, b"\0\0\1\4\2\xfe")) + assert policy._is_complete_png(indexed_png(2, 1, 1, 2, b"\0\x40")) + assert not policy._is_complete_png(indexed_png(2, 1, 1, 1, b"\0\x40")) + assert policy._is_complete_png(indexed_png(1, 1, 8, 1, b"\0\0", interlace=1)) + assert not policy._is_complete_png(indexed_png(1, 1, 8, 1, b"\0\1", interlace=1)) assert not policy._is_complete_png(png(gray, chunk(b"PLTE", b"\0\0\0"), chunk(b"IDAT", zlib.compress(b"\0\0")), end)) assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", b"not-zlib"), end)) assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\0")), end)) From 1520d301845b8aa680f6fe0a4fc6074f342b955e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 09:53:22 +0900 Subject: [PATCH 6/6] test(opencode): align head-advance trust contracts Signed-off-by: Seongho Bae --- tests/test_opencode_agent_contract.py | 7 ++++++- tests/test_pr_review_autofix_nvidia_nim_contract.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 79fdba39aa..0dc94a01ea 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -2660,7 +2660,12 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]' ) in metadata_step assert '[ "$live_head_repository" != "$TARGET_REPOSITORY" ]' not in metadata_step - assert '[ "$SUPPLIED_HEAD_SHA" = "$live_head_sha" ]' in metadata_step + assert 'mismatches+=("head_sha")' not in metadata_step + assert ( + 'if [ -n "$SUPPLIED_HEAD_SHA" ] && ' + '[ "$SUPPLIED_HEAD_SHA" != "$live_head_sha" ]; then' + ) in metadata_step + assert "repository_dispatch head advanced since dispatch" in metadata_step assert ( 'live_visibility="$(jq -r \'.base.repo.visibility // empty | ascii_downcase\'' ) in metadata_step diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 3dcfe2cdd8..68a0614c01 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -19,7 +19,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "2aa245e7f2a053a4c0b7a9cc8bac0d5d44d38092" +REVIEW_DISPATCH_BLOB_SHA = "3762183eb31c2805317362d2b2c2546e4fccdf09" def _workflow_text(path: Path) -> str: