diff --git a/CHANGELOG.md b/CHANGELOG.md index 2398ea5c..15e4e170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - API 에러 응답 생성 시 내부 예외 체인을 억제하여 의존성 오류나 내부 경로가 노출될 가능성을 줄임 - API 응답 미들웨어에 `Cache-Control: no-store, max-age=0` 헤더를 추가하여 민감한 파싱 데이터의 브라우저 및 중간 캐싱을 방지 - `uv.lock`의 의존성을 재잠금하여 실제 `pip-audit`/`trivy-fs` CVE를 제거: 런타임 경로의 `pillow` 12.2.0→12.3.0 (PYSEC-2026-3451/3452/3453/3454/3493/3494/3495/3496, 이미지 파서 취약점 8건), `pypdf>=6.15.0,<7.0` (lock 6.15.0; CVE-2026-59935/59936/59937/59938/71852/71870, PDF 파싱 경로), `click` 8.3.2→8.4.2 (PYSEC-2026-2132) — 모두 스캔 PDF/이미지 파싱 런타임에 직접 관련되며 선언 범위와 lock을 함께 고정함. 빌드 도구 `setuptools` 81.0.0→83.0.0 (CVE-2026-59890). 문서 툴체인의 `pymdown-extensions` 10.21.3→11.0.1 (CVE-2026-61632, MEDIUM)은 `mkdocs-material` 9.6.x의 `pymdown-extensions~=10.2`(`<11`) 상한 때문에 막혀 있었으므로, docs extra 핀을 `mkdocs-material>=9.7,<9.8`로 올려(9.7.x는 상한을 `>=10.2`로 완화) 해소함. `uv run mkdocs build --strict` 통과 확인. 조치 후 전체 잠금(런타임+extras) `pip-audit`: 취약점 0건. +- `pypdf`를 `>=6.16.1,<7.0`(lock 6.16.2)로 재잠금하여 신규 `trivy-fs` MEDIUM 3건을 제거: CVE-2026-84309(순환 `/Next` 아웃라인 트리로 `TreeObject.insert_child`가 무한 루프에 빠짐), CVE-2026-84310(`_get_outline`의 항목 수·중첩 깊이 제한 부재로 장시간·과다 메모리 소비), CVE-2026-84311(재사용 XObject 폼 그래프에 방문 여부 추적이 없어 텍스트 추출 시 경로가 지수적으로 증가) — 모두 신뢰할 수 없는 PDF 업로드를 처리하는 파서 가용성 취약점이며 pypdf 6.16.0/6.16.1에서 각각 수정됨. 조치 후 전체 잠금 `pip-audit`: 취약점 0건. ### Performance - `newsdom_api.dom_builder._html_safe_text` 함수에 early return과 타입 체크를 도입하여 불필요한 `str()` 캐스팅을 제거함으로써 처리 속도를 개선했습니다. diff --git a/docs/doctoring/dependency-security-baseline.md b/docs/doctoring/dependency-security-baseline.md index 2dc515c9..30b74528 100644 --- a/docs/doctoring/dependency-security-baseline.md +++ b/docs/doctoring/dependency-security-baseline.md @@ -14,12 +14,12 @@ The adopted floors are: - `setuptools>=83` for the build backend; - `Pillow>=12.3,<13.0` for image parsing on the untrusted document-ingestion path; -- `pypdf>=6.15.0,<7.0` for PDF parsing; +- `pypdf>=6.16.1,<7.0` for PDF parsing; - `mkdocs-material>=9.7,<9.8`, allowing `pymdown-extensions>=11` while the MkDocs core remains on the supported 1.x line. The generated lock additionally resolves Click 8.4.2, setuptools 83.0.0, -Pillow 12.3.0, pypdf 6.15.0, mkdocs-material 9.7.7, and +Pillow 12.3.0, pypdf 6.16.2, mkdocs-material 9.7.7, and pymdown-extensions 11.0.1. Direct floors prevent a later lock refresh from silently selecting known-vulnerable ranges again. @@ -36,6 +36,20 @@ successfully without suppressing either finding. The shared direct floor and loc therefore move together to 6.15.0 rather than hiding the findings in `.trivyignore`. +On September 3, 2026, an unrelated PR's `trivy-fs` gate reported three further +MEDIUM findings against the locked 6.15.0 artifact: CVE-2026-84309 (an attacker +-crafted PDF whose cyclic `/Next` outline-tree structure drives +`TreeObject.insert_child` into an infinite loop on a writing code path), +CVE-2026-84310 (unbounded entry-count and nesting-depth traversal in +`_get_outline` allowing long runtimes and large memory use), and CVE-2026-84311 +(a reused-XObject form graph without a memoized/visited guard in +`PageObject._extract_text`/`extract_xform_text`, producing exponentially many +traversal paths). All three are availability risks directly on NewsDOM's +untrusted PDF-ingestion path and are fixed upstream in pypdf 6.16.0/6.16.1 +respectively. The direct floor and lock move together to 6.16.1 (lock resolves +6.16.2, the latest compatible release at the time of this record) rather than +suppressing the findings. + CVE-2026-59890 affects setuptools versions before 83.0.0. On normalization-preserving macOS filesystems, specially named files could bypass `MANIFEST.in` exclusion matching and enter a source distribution. Although this @@ -46,7 +60,7 @@ Pillow 12.3.0 and pypdf release artifacts are distributed through PyPI with published cryptographic file digests. Those artifacts and digests provide provenance inputs; they do not by themselves establish that a package is safe. Repository scans, hash-locked resolution, current-head tests, and independent -review remain mandatory. PyPI's official JSON metadata confirms the 6.15.0 +review remain mandatory. PyPI's official JSON metadata confirms the 6.16.2 release and the artifact hashes recorded in this repository's generated lock. ## Secure-development and provenance controls @@ -129,6 +143,15 @@ Open Source Vulnerabilities. (2026c). *CVE-2026-71852*. Retrieved August 9, Open Source Vulnerabilities. (2026d). *CVE-2026-71870*. Retrieved August 9, 2026, from https://osv.dev/vulnerability/CVE-2026-71870 +Open Source Vulnerabilities. (2026e). *CVE-2026-84309*. Retrieved September 3, + 2026, from https://osv.dev/vulnerability/CVE-2026-84309 + +Open Source Vulnerabilities. (2026f). *CVE-2026-84310*. Retrieved September 3, + 2026, from https://osv.dev/vulnerability/CVE-2026-84310 + +Open Source Vulnerabilities. (2026g). *CVE-2026-84311*. Retrieved September 3, + 2026, from https://osv.dev/vulnerability/CVE-2026-84311 + Python Packaging Authority. (2026a). *Digital attestations*. PyPI Docs. Retrieved August 4, 2026, from https://docs.pypi.org/attestations/ @@ -140,3 +163,6 @@ Python Packaging Authority. (2026c). *pypdf 6.15.0*. Python Package Index. Python Packaging Authority. (2026d). *setuptools 83.0.0*. Python Package Index. Retrieved August 4, 2026, from https://pypi.org/project/setuptools/83.0.0/ + +Python Packaging Authority. (2026e). *pypdf 6.16.2*. Python Package Index. + Retrieved September 3, 2026, from https://pypi.org/project/pypdf/6.16.2/ diff --git a/pyproject.toml b/pyproject.toml index 7a29144e..9da77b53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "python-multipart>=0.0.31,<1.0", "reportlab>=4.2,<6.0", "Pillow>=12.3,<13.0", - "pypdf>=6.15.0,<7.0", + "pypdf>=6.16.1,<7.0", ] [project.optional-dependencies] diff --git a/tests/test_project_metadata.py b/tests/test_project_metadata.py index 324cb086..8f0abcbe 100644 --- a/tests/test_project_metadata.py +++ b/tests/test_project_metadata.py @@ -96,7 +96,7 @@ def test_security_dependency_floors_exclude_known_vulnerable_ranges(): dependencies_section = _dependencies_section(text) assert '"Pillow>=12.3,<13.0"' in dependencies_section - assert '"pypdf>=6.15.0,<7.0"' in dependencies_section + assert '"pypdf>=6.16.1,<7.0"' in dependencies_section assert 'requires = ["setuptools>=83", "wheel"]' in text @@ -202,4 +202,4 @@ def test_uv_lock_does_not_track_external_mineru_pipeline_runtime_stack(): def test_uv_lock_pins_pypdf_at_patched_release(): - assert _locked_package_version("pypdf") >= (6, 15, 0) + assert _locked_package_version("pypdf") >= (6, 16, 1) diff --git a/tests/test_pypdf_security_floor.py b/tests/test_pypdf_security_floor.py index 6a641e83..b01c876f 100644 --- a/tests/test_pypdf_security_floor.py +++ b/tests/test_pypdf_security_floor.py @@ -6,9 +6,13 @@ import yaml -_REQUIRED_PYPDF_VERSION = (6, 15, 0) -_CURRENT_PYPDF_CVES = ("CVE-2026-71852", "CVE-2026-71870") -_LOCKED_PYPDF_REQUIREMENT = '{ name = "pypdf", specifier = ">=6.15.0,<7.0" },' +_REQUIRED_PYPDF_VERSION = (6, 16, 1) +_CURRENT_PYPDF_CVES = ("CVE-2026-84309", "CVE-2026-84310", "CVE-2026-84311") +# Every literal below derives from the tuple above, so a future floor bump has +# exactly one place to change and the documentation checks cannot keep +# matching a superseded floor by accident. +_PYPDF_SPECIFIER = ">=" + ".".join(str(part) for part in _REQUIRED_PYPDF_VERSION) + ",<7.0" +_LOCKED_PYPDF_REQUIREMENT = f'{{ name = "pypdf", specifier = "{_PYPDF_SPECIFIER}" }},' def _locked_pypdf_version() -> tuple[int, ...]: @@ -24,10 +28,10 @@ def _locked_pypdf_version() -> tuple[int, ...]: def test_project_declares_current_pypdf_security_floor() -> None: - """Prevent future lock refreshes from selecting the vulnerable 6.14.x line.""" + """Prevent lock refreshes from selecting pypdf below the current floor.""" project_text = Path("pyproject.toml").read_text(encoding="utf-8") - assert '"pypdf>=6.15.0,<7.0"' in project_text + assert f'"pypdf{_PYPDF_SPECIFIER}"' in project_text def test_lock_uses_current_pypdf_security_release() -> None: @@ -61,7 +65,25 @@ def test_current_pypdf_advisories_and_floor_are_documented() -> None: for cve_id in _CURRENT_PYPDF_CVES: assert f"https://osv.dev/vulnerability/{cve_id}" in baseline - assert "`pypdf>=6.15.0,<7.0`" in changelog + + # The CHANGELOG entry writes the package and its specifier as separate + # inline-code spans; require both, plus the release the lock resolved, + # on one entry line rather than a single fused code span. + locked_release = ".".join(str(part) for part in _locked_pypdf_version()) + changelog_entry = re.compile( + r"`pypdf`[^\n]*`" + re.escape(_PYPDF_SPECIFIER) + r"`[^\n]*" + re.escape(locked_release) + ) + assert changelog_entry.search(changelog), ( + f"CHANGELOG.md lacks an entry naming `pypdf`, `{_PYPDF_SPECIFIER}` and lock {locked_release}" + ) + + # Markdown wraps the provenance sentence across lines; compare on + # whitespace-normalized text, and tie the release to the lock file. + provenance = ( + f"PyPI's official JSON metadata confirms the {locked_release} release and the " + "artifact hashes recorded in this repository's generated lock." + ) + assert provenance in re.sub(r"\s+", " ", baseline) def test_trivy_registry_exception_is_scoped_to_the_example_manifest() -> None: diff --git a/uv.lock b/uv.lock index a0d133b8..57d28387 100644 --- a/uv.lock +++ b/uv.lock @@ -643,7 +643,7 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.9,<3.0" }, { name = "pyinstaller", marker = "extra == 'fuzz'", specifier = "==6.21.0" }, { name = "pymdown-extensions", marker = "extra == 'docs'", specifier = ">=11,<12" }, - { name = "pypdf", specifier = ">=6.15.0,<7.0" }, + { name = "pypdf", specifier = ">=6.16.1,<7.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3,<10.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0,<8.0" }, @@ -929,14 +929,14 @@ wheels = [ [[package]] name = "pypdf" -version = "6.15.0" +version = "6.16.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/17/ee75a92718ec7212de831e71454d702225aa5e474a805cce169806044453/pypdf-6.15.0.tar.gz", hash = "sha256:d39c4d955a76409284a905e2d65b40076d77ab76129e0faaeeb6612403ecfc79", size = 6993794, upload-time = "2026-08-06T13:06:49.929Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/66/54212e75406afd9f3e933d0dda23072f6aecc55c5a273077dc2e0b028b23/pypdf-6.16.2.tar.gz", hash = "sha256:595647f6191de6f402cfde1d0c455d6cbccbd509aac32b34783009c032de5d6e", size = 7008996, upload-time = "2026-08-23T13:50:07.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/72/ce3067ac31e214a66388159f8462ddb8c13dd00170f24d555a1f1ae8ee91/pypdf-6.15.0-py3-none-any.whl", hash = "sha256:14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee", size = 378123, upload-time = "2026-08-06T13:06:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/13/f1/a2da3b55acd4ab737bf728c97edaaed5ec1d3c1236acb639dcdfa97e42c7/pypdf-6.16.2-py3-none-any.whl", hash = "sha256:c8b09a59399062fb45a1b8156c18a787a10a3dae03ac9674397a226712c94604", size = 385060, upload-time = "2026-08-23T13:50:05.349Z" }, ] [[package]]