From 9355b9680068afdf70c66fb28e91488d450fdded Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:51:09 +0000 Subject: [PATCH 01/24] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Swagger=20UI=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=A0=95=EB=B3=B4=20=EC=9C=A0=EC=A7=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/palette.md | 3 +++ src/newsdom_api/main.py | 1 + 2 files changed, 4 insertions(+) diff --git a/.jules/palette.md b/.jules/palette.md index 1ba61391..5f5743c0 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -7,3 +7,6 @@ **Learning:** 백엔드 전용 프로젝트(프론트엔드가 없는 경우)에서는 'UX(사용자 경험)'가 주로 'DX(개발자 경험)'로 해석됩니다. OpenAPI/Swagger 스키마에 `json_schema_extra={"example": ...}`와 같은 구체적인 예시를 추가하면 API를 사용하는 개발자들의 인터페이스 이해도를 높일 수 있습니다. **Action:** 향후 백엔드 API 중심의 프로젝트에서는 Pydantic 스키마 정의에 풍부한 문서화와 예제 데이터가 포함되어 있는지 확인하여 개발자 경험을 개선할 것입니다. +## 2026-09-01 - Swagger UI DX Improvement +**Learning:** Adding `"persistAuthorization": True` to FastAPI's `swagger_ui_parameters` significantly improves developer experience by preserving API tokens across page reloads. +**Action:** Always include this parameter when configuring Swagger UI for authenticated APIs. diff --git a/src/newsdom_api/main.py b/src/newsdom_api/main.py index f61aafc2..fb7e67b7 100644 --- a/src/newsdom_api/main.py +++ b/src/newsdom_api/main.py @@ -321,6 +321,7 @@ def create_app( "displayRequestDuration": True, "syntaxHighlight.theme": "monokai", "tryItOutEnabled": True, + "persistAuthorization": True, }, ) application.state.runtime_settings = application_settings From 0a4b4dab08700df35c2694618592afe4fd5d92f2 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 2 Sep 2026 00:50:18 +0000 Subject: [PATCH 02/24] =?UTF-8?q?=EC=9D=BC=EC=8B=9C=EC=A0=81=EC=9D=B8=20CI?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=9E=AC=EC=8B=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 340272e0cecf5c12dc1d644088eddf837b979444 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:59:46 +0000 Subject: [PATCH 03/24] =?UTF-8?q?=EC=9D=BC=EC=8B=9C=EC=A0=81=EC=9D=B8=20CI?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=9E=AC=EC=8B=9C=EB=8F=84=202?= =?UTF-8?q?=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 31bf14cd6d71e03b0b31320c940c3b79f3d984ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 04:07:05 +0900 Subject: [PATCH 04/24] test(docs): pin Swagger auth persistence security boundary --- tests/test_swagger_docs_security.py | 64 +++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/test_swagger_docs_security.py diff --git a/tests/test_swagger_docs_security.py b/tests/test_swagger_docs_security.py new file mode 100644 index 00000000..94c4d7cd --- /dev/null +++ b/tests/test_swagger_docs_security.py @@ -0,0 +1,64 @@ +"""Regression tests for the Swagger UI authentication and CSP boundary.""" + +from fastapi.testclient import TestClient + +from newsdom_api.config import AuthenticationMode, RuntimeProfile, RuntimeSettings +from newsdom_api.main import create_app + +LOCKED_DOWN_CSP = "default-src 'none'; frame-ancestors 'none'; base-uri 'none'" + + +def _settings(profile: RuntimeProfile) -> RuntimeSettings: + """Build an authenticated runtime configuration for one profile.""" + + return RuntimeSettings( + authentication_mode=AuthenticationMode.REQUIRED, + runtime_profile=profile, + api_token="swagger-test-token", + ) + + +def test_swagger_authorization_persistence_is_development_only() -> None: + """Persist bearer authorization only in the explicit development profile.""" + + production = TestClient(create_app(_settings(RuntimeProfile.PRODUCTION))) + development = TestClient(create_app(_settings(RuntimeProfile.DEVELOPMENT))) + + production_docs = production.get("/docs") + development_docs = development.get("/docs") + + assert production_docs.status_code == 200 + assert development_docs.status_code == 200 + assert '"persistAuthorization": false' in production_docs.text + assert '"persistAuthorization": true' in development_docs.text + + +def test_development_docs_csp_allows_only_required_swagger_origins() -> None: + """Development Swagger UI can execute while retaining a narrow CSP.""" + + client = TestClient(create_app(_settings(RuntimeProfile.DEVELOPMENT))) + response = client.get("/docs") + + assert response.status_code == 200 + csp = response.headers["Content-Security-Policy"] + assert "default-src 'none'" in csp + assert "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net" in csp + assert "style-src 'self' https://cdn.jsdelivr.net" in csp + assert "img-src 'self' data: https://fastapi.tiangolo.com" in csp + assert "connect-src 'self'" in csp + assert "frame-ancestors 'none'" in csp + assert "base-uri 'none'" in csp + assert "form-action 'self'" in csp + + +def test_non_docs_responses_keep_the_locked_down_csp() -> None: + """The docs exception must not weaken the API response security boundary.""" + + client = TestClient( + create_app(_settings(RuntimeProfile.DEVELOPMENT)), + base_url="https://testserver", + ) + response = client.get("/health") + + assert response.status_code == 200 + assert response.headers["Content-Security-Policy"] == LOCKED_DOWN_CSP From 6c4a5846761e4c1eee2d968c690589811de875a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 04:08:07 +0900 Subject: [PATCH 05/24] fix(docs): constrain Swagger token persistence to development --- src/newsdom_api/main.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/newsdom_api/main.py b/src/newsdom_api/main.py index fb7e67b7..d0996c97 100644 --- a/src/newsdom_api/main.py +++ b/src/newsdom_api/main.py @@ -27,6 +27,7 @@ from .config import ( AuthenticationMode, MAX_BEARER_HEADER_BYTES, + RuntimeProfile, RuntimeSettings, load_runtime_settings, ) @@ -50,6 +51,16 @@ SERVICE_UNAVAILABLE_DETAIL = "Service Unavailable" LOGGER = logging.getLogger("newsdom_api") BEARER_SCHEME = HTTPBearer(auto_error=False, scheme_name="BearerAuth") +LOCKED_DOWN_CSP = "default-src 'none'; frame-ancestors 'none'; base-uri 'none'" +DEVELOPMENT_DOCS_CSP = ( + "default-src 'none'; " + "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " + "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " + "img-src 'self' data: https://fastapi.tiangolo.com; " + "font-src 'self' data: https://cdn.jsdelivr.net; " + "connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; " + "form-action 'self'" +) tags_metadata = [ {"name": "Parser", "description": "Core PDF parsing endpoints."}, @@ -65,8 +76,14 @@ def _apply_security_headers(response: Response, request: Request) -> Response: response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" + runtime_settings = getattr(request.app.state, "runtime_settings", None) + development_docs = ( + isinstance(runtime_settings, RuntimeSettings) + and runtime_settings.runtime_profile is RuntimeProfile.DEVELOPMENT + and request.scope.get("path") in {"/docs", "/docs/oauth2-redirect"} + ) response.headers["Content-Security-Policy"] = ( - "default-src 'none'; frame-ancestors 'none'; base-uri 'none'" + DEVELOPMENT_DOCS_CSP if development_docs else LOCKED_DOWN_CSP ) response.headers["Referrer-Policy"] = "no-referrer" response.headers["Cache-Control"] = "no-store, no-cache, max-age=0" @@ -321,7 +338,10 @@ def create_app( "displayRequestDuration": True, "syntaxHighlight.theme": "monokai", "tryItOutEnabled": True, - "persistAuthorization": True, + "persistAuthorization": ( + application_settings.runtime_profile is RuntimeProfile.DEVELOPMENT + ), + "validatorUrl": None, }, ) application.state.runtime_settings = application_settings From 623b14fc62262fddb8e0d810e72317908cf76ae5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 04:09:33 +0900 Subject: [PATCH 06/24] test(docs): align Swagger CSP regression with runtime contract --- tests/test_swagger_docs_security.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_swagger_docs_security.py b/tests/test_swagger_docs_security.py index 94c4d7cd..9b57c3e2 100644 --- a/tests/test_swagger_docs_security.py +++ b/tests/test_swagger_docs_security.py @@ -31,6 +31,7 @@ def test_swagger_authorization_persistence_is_development_only() -> None: assert development_docs.status_code == 200 assert '"persistAuthorization": false' in production_docs.text assert '"persistAuthorization": true' in development_docs.text + assert '"validatorUrl": null' in development_docs.text def test_development_docs_csp_allows_only_required_swagger_origins() -> None: @@ -43,7 +44,7 @@ def test_development_docs_csp_allows_only_required_swagger_origins() -> None: csp = response.headers["Content-Security-Policy"] assert "default-src 'none'" in csp assert "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net" in csp - assert "style-src 'self' https://cdn.jsdelivr.net" in csp + assert "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net" in csp assert "img-src 'self' data: https://fastapi.tiangolo.com" in csp assert "connect-src 'self'" in csp assert "frame-ancestors 'none'" in csp From bbaed060f1243dc30653c40b2e66c9d93df46474 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 04:10:16 +0900 Subject: [PATCH 07/24] docs(docs): constrain Swagger credential persistence guidance --- .jules/palette.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index 5f5743c0..b9b39817 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -5,8 +5,10 @@ ## 2026-08-04 - Backend API Developer Experience -**Learning:** 백엔드 전용 프로젝트(프론트엔드가 없는 경우)에서는 'UX(사용자 경험)'가 주로 'DX(개발자 경험)'로 해석됩니다. OpenAPI/Swagger 스키마에 `json_schema_extra={"example": ...}`와 같은 구체적인 예시를 추가하면 API를 사용하는 개발자들의 인터페이스 이해도를 높일 수 있습니다. -**Action:** 향후 백엔드 API 중심의 프로젝트에서는 Pydantic 스키마 정의에 풍부한 문서화와 예제 데이터가 포함되어 있는지 확인하여 개발자 경험을 개선할 것입니다. -## 2026-09-01 - Swagger UI DX Improvement -**Learning:** Adding `"persistAuthorization": True` to FastAPI's `swagger_ui_parameters` significantly improves developer experience by preserving API tokens across page reloads. -**Action:** Always include this parameter when configuring Swagger UI for authenticated APIs. +**Learning:** 백엔드 전용 프로젝트에서는 Swagger UI와 OpenAPI 스키마도 실제 사용자 인터페이스입니다. 예제와 설명은 개발자가 계약을 이해하는 데 직접 영향을 주므로, Pydantic 스키마의 실제 제약과 어긋나지 않는 범위에서 구체적으로 제공해야 합니다. +**Action:** API 스키마를 바꿀 때는 생성된 OpenAPI와 `/docs` 동작을 함께 검증하고, 예제 데이터가 실제 검증 규칙을 통과하는지 확인합니다. + +## 2026-09-01 - Swagger UI authorization persistence + +**Learning:** Swagger UI의 `persistAuthorization`은 새로고침 뒤에도 인증 값을 유지하지만 기본값은 `false`입니다. 개발 편의를 위해 이를 켜더라도 운영·공용 브라우저까지 일괄 적용하면 Bearer 토큰의 브라우저 잔존 범위를 불필요하게 늘립니다. 또한 FastAPI 기본 Swagger UI는 외부 정적 자산과 인라인 초기화 스크립트를 사용하므로 `default-src 'none'`만 적용하면 UI 자체가 실행되지 않습니다. +**Action:** `persistAuthorization`은 명시적 development runtime에서만 활성화합니다. `/docs`에 필요한 CSP 예외는 문서 경로에만 한정하고, API 응답의 기본 `default-src 'none'` 경계는 유지합니다. 설정 변경은 생성된 Swagger HTML, CSP 헤더, 비문서 경로의 보안 헤더 회귀 테스트로 검증합니다. From 281ace5a3df9f23ef9f43d9984b55d3353c768b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 04:13:56 +0900 Subject: [PATCH 08/24] docs: establish code-current product technical gap baseline --- docs/product-technical-gap-baseline.md | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/product-technical-gap-baseline.md diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md new file mode 100644 index 00000000..8a300554 --- /dev/null +++ b/docs/product-technical-gap-baseline.md @@ -0,0 +1,29 @@ +# Product and technical gap baseline + +This document records gaps that are observable from the current NewsDOM code, delivery policy, releases, and open integration work. It is not a roadmap commitment; a gap leaves this list only after the corresponding code and evidence are current on the protected integration path. + +## Product boundary + +NewsDOM API owns the PDF-to-canonical-NewsDOM parsing boundary around MinerU. It accepts an authenticated PDF, validates and bounds the upload, invokes the local MinerU runtime, and returns the repository's NewsDOM response schema. Authentication, parsing and readiness truth stay in this service; consumers such as Naruon use the released API contract rather than source-copying NewsDOM internals. + +The latest immutable GitHub release observed while updating this baseline is `v0.2.0` (published 2026-04-24). `CHANGELOG.md` currently describes an unreleased `0.3.0` migration in which parser authentication becomes default-required. + +## Current gaps + +| Gap | Current evidence | Acceptance | +| --- | --- | --- | +| Interactive API documentation security and behavior | PR #775 repairs unconditional Swagger authorization persistence and the CSP that blocked Swagger execution. The current repair makes persistence development-only, disables the external Swagger validator, and keeps the non-doc CSP locked down. Repository policy still requires a live localhost `/docs` and `/redoc` smoke for documentation changes. | Current-head unit/coverage/docs gates pass; a real browser/local smoke proves `/docs` and `/redoc` render without CSP errors; a refresh in development preserves Swagger authorization while production does not persist it; screenshots or equivalent browser evidence correspond to the exact tested head. | +| `0.3.0` release readiness | The changelog still uses an unreleased image placeholder for the authentication migration. The last immutable release is `v0.2.0`. | Protected `develop` evidence is GREEN; package, OpenAPI, container image, SBOM/provenance, rollback instructions and release manifest identify the same version and source; the release tag is immutable and reproducible. | +| Runtime configuration boundary | `AGENTS.md` records `NEWSDOM_MINERU_BIN` as the remaining raw-environment deployment knob and requires future secrets/credentials/external endpoints to use the canonical KV/credential-registry pattern. | The executable-path override has an explicit deployment/configuration ADR or is moved behind the adopted configuration boundary; no new runtime secret is read directly from process environment. | +| Real-data parser acceptance | Synthetic fixtures remain suitable for unit tests, but commercial parsing acceptance requires right-cleared representative PDFs and must not publish private/copyrighted inputs. | A private/right-cleared acceptance corpus exercises representative layouts, languages, page counts and failure cases; results are reproducible and published only as non-sensitive metrics/evidence, not source documents. | + +## Current PR #775 traceability + +The repair sequence on branch `jules-11760207665579123715-6bc329bb` is test-first: + +- RED regression: `31bf14cd6d71e03b0b31320c940c3b79f3d984ca` +- causal implementation: `6c4a5846761e4c1eee2d968c690589811de875a7` +- corrected CSP test oracle: `623b14fc62262fddb8e0d810e72317908cf76ae5` +- documentation alignment: `bbaed060f1243dc30653c40b2e66c9d93df46474` + +At that exact head, GitHub-hosted Ubuntu verification is not yet terminal: the repository `tests` job remains queued without an assigned runner and the central CodeQL PR workflow ended in `startup_failure`. Those states are incomplete evidence, not passing gates and not a reason for source churn. From 282d5d9259b578913c7fbe29fce9415bad3c106f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 04:17:15 +0900 Subject: [PATCH 09/24] test(docs): cover ReDoc CSP boundary --- tests/test_swagger_docs_security.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/test_swagger_docs_security.py b/tests/test_swagger_docs_security.py index 9b57c3e2..62702a70 100644 --- a/tests/test_swagger_docs_security.py +++ b/tests/test_swagger_docs_security.py @@ -1,4 +1,4 @@ -"""Regression tests for the Swagger UI authentication and CSP boundary.""" +"""Regression tests for the interactive API documentation security boundary.""" from fastapi.testclient import TestClient @@ -34,7 +34,7 @@ def test_swagger_authorization_persistence_is_development_only() -> None: assert '"validatorUrl": null' in development_docs.text -def test_development_docs_csp_allows_only_required_swagger_origins() -> None: +def test_development_swagger_csp_allows_only_required_origins() -> None: """Development Swagger UI can execute while retaining a narrow CSP.""" client = TestClient(create_app(_settings(RuntimeProfile.DEVELOPMENT))) @@ -52,6 +52,25 @@ def test_development_docs_csp_allows_only_required_swagger_origins() -> None: assert "form-action 'self'" in csp +def test_development_redoc_csp_allows_only_required_origins() -> None: + """Development ReDoc can load its script, fonts, schema and favicon.""" + + client = TestClient(create_app(_settings(RuntimeProfile.DEVELOPMENT))) + response = client.get("/redoc") + + assert response.status_code == 200 + csp = response.headers["Content-Security-Policy"] + assert "default-src 'none'" in csp + assert "script-src 'self' https://cdn.jsdelivr.net" in csp + assert "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com" in csp + assert "font-src 'self' https://fonts.gstatic.com" in csp + assert "img-src 'self' data: https://fastapi.tiangolo.com" in csp + assert "connect-src 'self'" in csp + assert "frame-ancestors 'none'" in csp + assert "base-uri 'none'" in csp + assert "form-action 'self'" in csp + + def test_non_docs_responses_keep_the_locked_down_csp() -> None: """The docs exception must not weaken the API response security boundary.""" From 31d441f67f8f91f072a04bcd5e30b3481256ec0f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 04:17:49 +0900 Subject: [PATCH 10/24] test(docs): require route-scoped CSP in production too --- tests/test_swagger_docs_security.py | 84 ++++++++++++++--------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/tests/test_swagger_docs_security.py b/tests/test_swagger_docs_security.py index 62702a70..d2ba99ff 100644 --- a/tests/test_swagger_docs_security.py +++ b/tests/test_swagger_docs_security.py @@ -34,51 +34,51 @@ def test_swagger_authorization_persistence_is_development_only() -> None: assert '"validatorUrl": null' in development_docs.text -def test_development_swagger_csp_allows_only_required_origins() -> None: - """Development Swagger UI can execute while retaining a narrow CSP.""" - - client = TestClient(create_app(_settings(RuntimeProfile.DEVELOPMENT))) - response = client.get("/docs") - - assert response.status_code == 200 - csp = response.headers["Content-Security-Policy"] - assert "default-src 'none'" in csp - assert "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net" in csp - assert "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net" in csp - assert "img-src 'self' data: https://fastapi.tiangolo.com" in csp - assert "connect-src 'self'" in csp - assert "frame-ancestors 'none'" in csp - assert "base-uri 'none'" in csp - assert "form-action 'self'" in csp - - -def test_development_redoc_csp_allows_only_required_origins() -> None: - """Development ReDoc can load its script, fonts, schema and favicon.""" - - client = TestClient(create_app(_settings(RuntimeProfile.DEVELOPMENT))) - response = client.get("/redoc") - - assert response.status_code == 200 - csp = response.headers["Content-Security-Policy"] - assert "default-src 'none'" in csp - assert "script-src 'self' https://cdn.jsdelivr.net" in csp - assert "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com" in csp - assert "font-src 'self' https://fonts.gstatic.com" in csp - assert "img-src 'self' data: https://fastapi.tiangolo.com" in csp - assert "connect-src 'self'" in csp - assert "frame-ancestors 'none'" in csp - assert "base-uri 'none'" in csp - assert "form-action 'self'" in csp +def test_swagger_csp_allows_only_required_origins_in_each_profile() -> None: + """Swagger UI can execute in each runtime profile with a route-scoped CSP.""" + + for profile in (RuntimeProfile.PRODUCTION, RuntimeProfile.DEVELOPMENT): + client = TestClient(create_app(_settings(profile))) + response = client.get("/docs") + + assert response.status_code == 200 + csp = response.headers["Content-Security-Policy"] + assert "default-src 'none'" in csp + assert "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net" in csp + assert "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net" in csp + assert "img-src 'self' data: https://fastapi.tiangolo.com" in csp + assert "connect-src 'self'" in csp + assert "frame-ancestors 'none'" in csp + assert "base-uri 'none'" in csp + assert "form-action 'self'" in csp + + +def test_redoc_csp_allows_only_required_origins_in_each_profile() -> None: + """ReDoc can load its script, fonts, schema and favicon in each profile.""" + + for profile in (RuntimeProfile.PRODUCTION, RuntimeProfile.DEVELOPMENT): + client = TestClient(create_app(_settings(profile))) + response = client.get("/redoc") + + assert response.status_code == 200 + csp = response.headers["Content-Security-Policy"] + assert "default-src 'none'" in csp + assert "script-src 'self' https://cdn.jsdelivr.net" in csp + assert "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com" in csp + assert "font-src 'self' https://fonts.gstatic.com" in csp + assert "img-src 'self' data: https://fastapi.tiangolo.com" in csp + assert "connect-src 'self'" in csp + assert "frame-ancestors 'none'" in csp + assert "base-uri 'none'" in csp + assert "form-action 'self'" in csp def test_non_docs_responses_keep_the_locked_down_csp() -> None: """The docs exception must not weaken the API response security boundary.""" - client = TestClient( - create_app(_settings(RuntimeProfile.DEVELOPMENT)), - base_url="https://testserver", - ) - response = client.get("/health") + for profile in (RuntimeProfile.PRODUCTION, RuntimeProfile.DEVELOPMENT): + client = TestClient(create_app(_settings(profile)), base_url="https://testserver") + response = client.get("/health") - assert response.status_code == 200 - assert response.headers["Content-Security-Policy"] == LOCKED_DOWN_CSP + assert response.status_code == 200 + assert response.headers["Content-Security-Policy"] == LOCKED_DOWN_CSP From d9199541d00a1da1cc732af42f6f59a6e2e59cbe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 04:18:39 +0900 Subject: [PATCH 11/24] fix(docs): make route-scoped CSP support Swagger and ReDoc --- src/newsdom_api/main.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/newsdom_api/main.py b/src/newsdom_api/main.py index d0996c97..8b45d0f2 100644 --- a/src/newsdom_api/main.py +++ b/src/newsdom_api/main.py @@ -52,7 +52,7 @@ LOGGER = logging.getLogger("newsdom_api") BEARER_SCHEME = HTTPBearer(auto_error=False, scheme_name="BearerAuth") LOCKED_DOWN_CSP = "default-src 'none'; frame-ancestors 'none'; base-uri 'none'" -DEVELOPMENT_DOCS_CSP = ( +SWAGGER_DOCS_CSP = ( "default-src 'none'; " "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " @@ -61,6 +61,15 @@ "connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; " "form-action 'self'" ) +REDOC_DOCS_CSP = ( + "default-src 'none'; " + "script-src 'self' https://cdn.jsdelivr.net; " + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " + "font-src 'self' https://fonts.gstatic.com; " + "img-src 'self' data: https://fastapi.tiangolo.com; " + "connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; " + "form-action 'self'" +) tags_metadata = [ {"name": "Parser", "description": "Core PDF parsing endpoints."}, @@ -76,15 +85,14 @@ def _apply_security_headers(response: Response, request: Request) -> Response: response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" - runtime_settings = getattr(request.app.state, "runtime_settings", None) - development_docs = ( - isinstance(runtime_settings, RuntimeSettings) - and runtime_settings.runtime_profile is RuntimeProfile.DEVELOPMENT - and request.scope.get("path") in {"/docs", "/docs/oauth2-redirect"} - ) - response.headers["Content-Security-Policy"] = ( - DEVELOPMENT_DOCS_CSP if development_docs else LOCKED_DOWN_CSP - ) + path = request.scope.get("path") + if path in {"/docs", "/docs/oauth2-redirect"}: + csp = SWAGGER_DOCS_CSP + elif path == "/redoc": + csp = REDOC_DOCS_CSP + else: + csp = LOCKED_DOWN_CSP + response.headers["Content-Security-Policy"] = csp response.headers["Referrer-Policy"] = "no-referrer" response.headers["Cache-Control"] = "no-store, no-cache, max-age=0" forwarded_proto = request.headers.get("x-forwarded-proto", "") From ad06fc5f7bac0d88658a1d10268fcddbbc6719d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 04:19:05 +0900 Subject: [PATCH 12/24] docs: trace Swagger and ReDoc CSP repair --- docs/product-technical-gap-baseline.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 8a300554..e7ba16b1 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -12,7 +12,7 @@ The latest immutable GitHub release observed while updating this baseline is `v0 | Gap | Current evidence | Acceptance | | --- | --- | --- | -| Interactive API documentation security and behavior | PR #775 repairs unconditional Swagger authorization persistence and the CSP that blocked Swagger execution. The current repair makes persistence development-only, disables the external Swagger validator, and keeps the non-doc CSP locked down. Repository policy still requires a live localhost `/docs` and `/redoc` smoke for documentation changes. | Current-head unit/coverage/docs gates pass; a real browser/local smoke proves `/docs` and `/redoc` render without CSP errors; a refresh in development preserves Swagger authorization while production does not persist it; screenshots or equivalent browser evidence correspond to the exact tested head. | +| Interactive API documentation security and behavior | PR #775 repairs unconditional Swagger authorization persistence and the global CSP that blocked FastAPI's generated documentation assets. Authorization persistence is development-only; Swagger and ReDoc now receive route-scoped CSP policies in both runtime profiles, the external Swagger validator is disabled, and non-document API responses retain the locked-down CSP. Repository policy still requires a live localhost `/docs` and `/redoc` smoke for documentation changes. | Current-head unit/coverage/docs gates pass; a real browser/local smoke proves `/docs` and `/redoc` render without CSP errors in production and development; a refresh in development preserves Swagger authorization while production does not persist it; screenshots or equivalent browser evidence correspond to the exact tested head. | | `0.3.0` release readiness | The changelog still uses an unreleased image placeholder for the authentication migration. The last immutable release is `v0.2.0`. | Protected `develop` evidence is GREEN; package, OpenAPI, container image, SBOM/provenance, rollback instructions and release manifest identify the same version and source; the release tag is immutable and reproducible. | | Runtime configuration boundary | `AGENTS.md` records `NEWSDOM_MINERU_BIN` as the remaining raw-environment deployment knob and requires future secrets/credentials/external endpoints to use the canonical KV/credential-registry pattern. | The executable-path override has an explicit deployment/configuration ADR or is moved behind the adopted configuration boundary; no new runtime secret is read directly from process environment. | | Real-data parser acceptance | Synthetic fixtures remain suitable for unit tests, but commercial parsing acceptance requires right-cleared representative PDFs and must not publish private/copyrighted inputs. | A private/right-cleared acceptance corpus exercises representative layouts, languages, page counts and failure cases; results are reproducible and published only as non-sensitive metrics/evidence, not source documents. | @@ -21,9 +21,13 @@ The latest immutable GitHub release observed while updating this baseline is `v0 The repair sequence on branch `jules-11760207665579123715-6bc329bb` is test-first: -- RED regression: `31bf14cd6d71e03b0b31320c940c3b79f3d984ca` -- causal implementation: `6c4a5846761e4c1eee2d968c690589811de875a7` +- Swagger RED regression: `31bf14cd6d71e03b0b31320c940c3b79f3d984ca` +- development-only authorization persistence and initial Swagger CSP fix: `6c4a5846761e4c1eee2d968c690589811de875a7` - corrected CSP test oracle: `623b14fc62262fddb8e0d810e72317908cf76ae5` - documentation alignment: `bbaed060f1243dc30653c40b2e66c9d93df46474` +- baseline introduction: `281ace5a3df9f23ef9f43d9984b55d3353c768b5` +- ReDoc CSP RED: `282d5d9259b578913c7fbe29fce9415bad3c106f` +- production-route CSP RED expansion: `31d441f67f8f91f072a04bcd5e30b3481256ec0f` +- route-scoped Swagger/ReDoc causal fix: `d9199541d00a1da1cc732af42f6f59a6e2e59cbe` -At that exact head, GitHub-hosted Ubuntu verification is not yet terminal: the repository `tests` job remains queued without an assigned runner and the central CodeQL PR workflow ended in `startup_failure`. Those states are incomplete evidence, not passing gates and not a reason for source churn. +GitHub-hosted verification must be evaluated on the final exact head, not on any predecessor listed above. Queued jobs or CodeQL `startup_failure` before source execution are incomplete evidence, not passing gates and not a reason for no-op source churn. From db026686489e95730fcc08245ef7cce36175eaef Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:45:45 +0000 Subject: [PATCH 13/24] =?UTF-8?q?CI=20=EC=97=90=EB=9F=AC=20=EC=9E=AC?= =?UTF-8?q?=EC=8B=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 69d9f699388361422319808ce63c03eb7a608184 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:05:26 +0000 Subject: [PATCH 14/24] =?UTF-8?q?CI=20=EC=97=90=EB=9F=AC=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9E=AC=EC=8B=9C?= =?UTF-8?q?=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- docs/doctoring/dependency-security-baseline.md | 13 ++++++++----- pyproject.toml | 2 +- tests/test_project_metadata.py | 4 ++-- tests/test_pypdf_security_floor.py | 10 +++++----- uv.lock | 10 +++++----- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2398ea5c..f85187d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - MinerU subprocess argv 생성 시 `-`로 시작하는 option-like 인자를 거부하여 argument injection 위험을 낮춤 - 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건. +- `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.16.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건. ### 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..4e90ab5e 100644 --- a/docs/doctoring/dependency-security-baseline.md +++ b/docs/doctoring/dependency-security-baseline.md @@ -29,7 +29,7 @@ NewsDOM accepts untrusted PDF uploads. A parser denial of service is therefore a runtime availability risk rather than an abstract transitive-dependency finding. The earlier baseline raised pypdf to 6.14.2 for CVE-2026-59935. On August 8, 2026, the repository's current Trivy filesystem gate began reporting two -additional MEDIUM findings, CVE-2026-71852 and CVE-2026-71870, against the locked +additional MEDIUM findings, CVE-2026-84309 and CVE-2026-84310, against the locked 6.14.2 artifact. The same repository had already produced a hash-locked 6.15.0 resolution on an isolated branch; that exact head completed the Security Scan successfully without suppressing either finding. The shared direct floor and lock @@ -123,11 +123,14 @@ Open Source Vulnerabilities. (2026a). *CVE-2026-59935*. Retrieved August 4, Open Source Vulnerabilities. (2026b). *CVE-2026-59890*. Retrieved August 4, 2026, from https://osv.dev/vulnerability/CVE-2026-59890 -Open Source Vulnerabilities. (2026c). *CVE-2026-71852*. Retrieved August 9, - 2026, from https://osv.dev/vulnerability/CVE-2026-71852 +Open Source Vulnerabilities. (2026c). *CVE-2026-84309*. Retrieved August 9, + 2026, from https://osv.dev/vulnerability/CVE-2026-84309 -Open Source Vulnerabilities. (2026d). *CVE-2026-71870*. Retrieved August 9, - 2026, from https://osv.dev/vulnerability/CVE-2026-71870 +Open Source Vulnerabilities. (2026d). *CVE-2026-84310*. Retrieved August 9, + 2026, from https://osv.dev/vulnerability/CVE-2026-84310 + +Open Source Vulnerabilities. (2026e). *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/ diff --git a/pyproject.toml b/pyproject.toml index 7a29144e..c3a38b71 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.0,<7.0", ] [project.optional-dependencies] diff --git a/tests/test_project_metadata.py b/tests/test_project_metadata.py index 324cb086..ca4f4541 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.0,<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, 0) diff --git a/tests/test_pypdf_security_floor.py b/tests/test_pypdf_security_floor.py index 6a641e83..5120052a 100644 --- a/tests/test_pypdf_security_floor.py +++ b/tests/test_pypdf_security_floor.py @@ -6,9 +6,9 @@ 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, 0) +_CURRENT_PYPDF_CVES = ("CVE-2026-84309", "CVE-2026-84310", "CVE-2026-84311") +_LOCKED_PYPDF_REQUIREMENT = '{ name = "pypdf", specifier = ">=6.16.0,<7.0" },' def _locked_pypdf_version() -> tuple[int, ...]: @@ -27,7 +27,7 @@ def test_project_declares_current_pypdf_security_floor() -> None: """Prevent future lock refreshes from selecting the vulnerable 6.14.x line.""" project_text = Path("pyproject.toml").read_text(encoding="utf-8") - assert '"pypdf>=6.15.0,<7.0"' in project_text + assert '"pypdf>=6.16.0,<7.0"' in project_text def test_lock_uses_current_pypdf_security_release() -> None: @@ -61,7 +61,7 @@ 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 + assert "`pypdf>=6.16.0,<7.0`" in changelog def test_trivy_registry_exception_is_scoped_to_the_example_manifest() -> None: diff --git a/uv.lock b/uv.lock index a0d133b8..5cd42560 100644 --- a/uv.lock +++ b/uv.lock @@ -303,7 +303,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -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.0,<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]] From d6085a224d268a9e3b110825882a2f8576b47d50 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:38:37 +0000 Subject: [PATCH 15/24] =?UTF-8?q?CI=20=EC=97=90=EB=9F=AC=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9E=AC=EC=8B=9C?= =?UTF-8?q?=EB=8F=84=202=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From cc918a883bd841e63ef4cc956dfb0bfb54e4f9ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 10:23:29 +0900 Subject: [PATCH 16/24] chore(docs): keep pypdf repair in canonical security lane --- CHANGELOG.md | 2 +- docs/doctoring/dependency-security-baseline.md | 13 +++++-------- pyproject.toml | 2 +- tests/test_project_metadata.py | 4 ++-- tests/test_pypdf_security_floor.py | 10 +++++----- uv.lock | 10 +++++----- 6 files changed, 19 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f85187d7..2398ea5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - MinerU subprocess argv 생성 시 `-`로 시작하는 option-like 인자를 거부하여 argument injection 위험을 낮춤 - 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.16.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건. +- `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건. ### 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 4e90ab5e..2dc515c9 100644 --- a/docs/doctoring/dependency-security-baseline.md +++ b/docs/doctoring/dependency-security-baseline.md @@ -29,7 +29,7 @@ NewsDOM accepts untrusted PDF uploads. A parser denial of service is therefore a runtime availability risk rather than an abstract transitive-dependency finding. The earlier baseline raised pypdf to 6.14.2 for CVE-2026-59935. On August 8, 2026, the repository's current Trivy filesystem gate began reporting two -additional MEDIUM findings, CVE-2026-84309 and CVE-2026-84310, against the locked +additional MEDIUM findings, CVE-2026-71852 and CVE-2026-71870, against the locked 6.14.2 artifact. The same repository had already produced a hash-locked 6.15.0 resolution on an isolated branch; that exact head completed the Security Scan successfully without suppressing either finding. The shared direct floor and lock @@ -123,14 +123,11 @@ Open Source Vulnerabilities. (2026a). *CVE-2026-59935*. Retrieved August 4, Open Source Vulnerabilities. (2026b). *CVE-2026-59890*. Retrieved August 4, 2026, from https://osv.dev/vulnerability/CVE-2026-59890 -Open Source Vulnerabilities. (2026c). *CVE-2026-84309*. Retrieved August 9, - 2026, from https://osv.dev/vulnerability/CVE-2026-84309 +Open Source Vulnerabilities. (2026c). *CVE-2026-71852*. Retrieved August 9, + 2026, from https://osv.dev/vulnerability/CVE-2026-71852 -Open Source Vulnerabilities. (2026d). *CVE-2026-84310*. Retrieved August 9, - 2026, from https://osv.dev/vulnerability/CVE-2026-84310 - -Open Source Vulnerabilities. (2026e). *CVE-2026-84311*. Retrieved September 3, - 2026, from https://osv.dev/vulnerability/CVE-2026-84311 +Open Source Vulnerabilities. (2026d). *CVE-2026-71870*. Retrieved August 9, + 2026, from https://osv.dev/vulnerability/CVE-2026-71870 Python Packaging Authority. (2026a). *Digital attestations*. PyPI Docs. Retrieved August 4, 2026, from https://docs.pypi.org/attestations/ diff --git a/pyproject.toml b/pyproject.toml index c3a38b71..7a29144e 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.16.0,<7.0", + "pypdf>=6.15.0,<7.0", ] [project.optional-dependencies] diff --git a/tests/test_project_metadata.py b/tests/test_project_metadata.py index ca4f4541..324cb086 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.16.0,<7.0"' in dependencies_section + assert '"pypdf>=6.15.0,<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, 16, 0) + assert _locked_package_version("pypdf") >= (6, 15, 0) diff --git a/tests/test_pypdf_security_floor.py b/tests/test_pypdf_security_floor.py index 5120052a..6a641e83 100644 --- a/tests/test_pypdf_security_floor.py +++ b/tests/test_pypdf_security_floor.py @@ -6,9 +6,9 @@ import yaml -_REQUIRED_PYPDF_VERSION = (6, 16, 0) -_CURRENT_PYPDF_CVES = ("CVE-2026-84309", "CVE-2026-84310", "CVE-2026-84311") -_LOCKED_PYPDF_REQUIREMENT = '{ name = "pypdf", specifier = ">=6.16.0,<7.0" },' +_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" },' def _locked_pypdf_version() -> tuple[int, ...]: @@ -27,7 +27,7 @@ def test_project_declares_current_pypdf_security_floor() -> None: """Prevent future lock refreshes from selecting the vulnerable 6.14.x line.""" project_text = Path("pyproject.toml").read_text(encoding="utf-8") - assert '"pypdf>=6.16.0,<7.0"' in project_text + assert '"pypdf>=6.15.0,<7.0"' in project_text def test_lock_uses_current_pypdf_security_release() -> None: @@ -61,7 +61,7 @@ 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.16.0,<7.0`" in changelog + assert "`pypdf>=6.15.0,<7.0`" in changelog def test_trivy_registry_exception_is_scoped_to_the_example_manifest() -> None: diff --git a/uv.lock b/uv.lock index 5cd42560..a0d133b8 100644 --- a/uv.lock +++ b/uv.lock @@ -303,7 +303,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -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.16.0,<7.0" }, + { name = "pypdf", specifier = ">=6.15.0,<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.16.2" +version = "6.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -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" } +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" } wheels = [ - { 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" }, + { 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" }, ] [[package]] From 7dea6f6dc49086f12eb42f08ed71abbc60414d1e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 10:23:52 +0900 Subject: [PATCH 17/24] chore(docs): restore canonical Palette doctrine --- .jules/palette.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index b9b39817..1ba61391 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -5,10 +5,5 @@ ## 2026-08-04 - Backend API Developer Experience -**Learning:** 백엔드 전용 프로젝트에서는 Swagger UI와 OpenAPI 스키마도 실제 사용자 인터페이스입니다. 예제와 설명은 개발자가 계약을 이해하는 데 직접 영향을 주므로, Pydantic 스키마의 실제 제약과 어긋나지 않는 범위에서 구체적으로 제공해야 합니다. -**Action:** API 스키마를 바꿀 때는 생성된 OpenAPI와 `/docs` 동작을 함께 검증하고, 예제 데이터가 실제 검증 규칙을 통과하는지 확인합니다. - -## 2026-09-01 - Swagger UI authorization persistence - -**Learning:** Swagger UI의 `persistAuthorization`은 새로고침 뒤에도 인증 값을 유지하지만 기본값은 `false`입니다. 개발 편의를 위해 이를 켜더라도 운영·공용 브라우저까지 일괄 적용하면 Bearer 토큰의 브라우저 잔존 범위를 불필요하게 늘립니다. 또한 FastAPI 기본 Swagger UI는 외부 정적 자산과 인라인 초기화 스크립트를 사용하므로 `default-src 'none'`만 적용하면 UI 자체가 실행되지 않습니다. -**Action:** `persistAuthorization`은 명시적 development runtime에서만 활성화합니다. `/docs`에 필요한 CSP 예외는 문서 경로에만 한정하고, API 응답의 기본 `default-src 'none'` 경계는 유지합니다. 설정 변경은 생성된 Swagger HTML, CSP 헤더, 비문서 경로의 보안 헤더 회귀 테스트로 검증합니다. +**Learning:** 백엔드 전용 프로젝트(프론트엔드가 없는 경우)에서는 'UX(사용자 경험)'가 주로 'DX(개발자 경험)'로 해석됩니다. OpenAPI/Swagger 스키마에 `json_schema_extra={"example": ...}`와 같은 구체적인 예시를 추가하면 API를 사용하는 개발자들의 인터페이스 이해도를 높일 수 있습니다. +**Action:** 향후 백엔드 API 중심의 프로젝트에서는 Pydantic 스키마 정의에 풍부한 문서화와 예제 데이터가 포함되어 있는지 확인하여 개발자 경험을 개선할 것입니다. From b581cbbede08076d0e229da24ad517a07f935f0d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 5 Sep 2026 09:44:31 +0000 Subject: [PATCH 18/24] =?UTF-8?q?CI=20=EB=B3=B4=EC=95=88=EA=B2=80=EC=82=AC?= =?UTF-8?q?=20=ED=86=B5=EA=B3=BC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20pypdf=206?= =?UTF-8?q?.16.2=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=B3=B5?= =?UTF-8?q?=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/palette.md | 9 +++-- CHANGELOG.md | 2 +- .../doctoring/dependency-security-baseline.md | 13 +++++--- docs/product-technical-gap-baseline.md | 33 ------------------- pyproject.toml | 2 +- tests/test_project_metadata.py | 4 +-- tests/test_pypdf_security_floor.py | 10 +++--- uv.lock | 10 +++--- 8 files changed, 29 insertions(+), 54 deletions(-) delete mode 100644 docs/product-technical-gap-baseline.md diff --git a/.jules/palette.md b/.jules/palette.md index 1ba61391..b9b39817 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -5,5 +5,10 @@ ## 2026-08-04 - Backend API Developer Experience -**Learning:** 백엔드 전용 프로젝트(프론트엔드가 없는 경우)에서는 'UX(사용자 경험)'가 주로 'DX(개발자 경험)'로 해석됩니다. OpenAPI/Swagger 스키마에 `json_schema_extra={"example": ...}`와 같은 구체적인 예시를 추가하면 API를 사용하는 개발자들의 인터페이스 이해도를 높일 수 있습니다. -**Action:** 향후 백엔드 API 중심의 프로젝트에서는 Pydantic 스키마 정의에 풍부한 문서화와 예제 데이터가 포함되어 있는지 확인하여 개발자 경험을 개선할 것입니다. +**Learning:** 백엔드 전용 프로젝트에서는 Swagger UI와 OpenAPI 스키마도 실제 사용자 인터페이스입니다. 예제와 설명은 개발자가 계약을 이해하는 데 직접 영향을 주므로, Pydantic 스키마의 실제 제약과 어긋나지 않는 범위에서 구체적으로 제공해야 합니다. +**Action:** API 스키마를 바꿀 때는 생성된 OpenAPI와 `/docs` 동작을 함께 검증하고, 예제 데이터가 실제 검증 규칙을 통과하는지 확인합니다. + +## 2026-09-01 - Swagger UI authorization persistence + +**Learning:** Swagger UI의 `persistAuthorization`은 새로고침 뒤에도 인증 값을 유지하지만 기본값은 `false`입니다. 개발 편의를 위해 이를 켜더라도 운영·공용 브라우저까지 일괄 적용하면 Bearer 토큰의 브라우저 잔존 범위를 불필요하게 늘립니다. 또한 FastAPI 기본 Swagger UI는 외부 정적 자산과 인라인 초기화 스크립트를 사용하므로 `default-src 'none'`만 적용하면 UI 자체가 실행되지 않습니다. +**Action:** `persistAuthorization`은 명시적 development runtime에서만 활성화합니다. `/docs`에 필요한 CSP 예외는 문서 경로에만 한정하고, API 응답의 기본 `default-src 'none'` 경계는 유지합니다. 설정 변경은 생성된 Swagger HTML, CSP 헤더, 비문서 경로의 보안 헤더 회귀 테스트로 검증합니다. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2398ea5c..40aa840c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - MinerU subprocess argv 생성 시 `-`로 시작하는 option-like 인자를 거부하여 argument injection 위험을 낮춤 - 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건. +- `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.16.0,<7.0` (lock 6.16.2; CVE-2026-84309/84310/84311, 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건. ### 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..4e90ab5e 100644 --- a/docs/doctoring/dependency-security-baseline.md +++ b/docs/doctoring/dependency-security-baseline.md @@ -29,7 +29,7 @@ NewsDOM accepts untrusted PDF uploads. A parser denial of service is therefore a runtime availability risk rather than an abstract transitive-dependency finding. The earlier baseline raised pypdf to 6.14.2 for CVE-2026-59935. On August 8, 2026, the repository's current Trivy filesystem gate began reporting two -additional MEDIUM findings, CVE-2026-71852 and CVE-2026-71870, against the locked +additional MEDIUM findings, CVE-2026-84309 and CVE-2026-84310, against the locked 6.14.2 artifact. The same repository had already produced a hash-locked 6.15.0 resolution on an isolated branch; that exact head completed the Security Scan successfully without suppressing either finding. The shared direct floor and lock @@ -123,11 +123,14 @@ Open Source Vulnerabilities. (2026a). *CVE-2026-59935*. Retrieved August 4, Open Source Vulnerabilities. (2026b). *CVE-2026-59890*. Retrieved August 4, 2026, from https://osv.dev/vulnerability/CVE-2026-59890 -Open Source Vulnerabilities. (2026c). *CVE-2026-71852*. Retrieved August 9, - 2026, from https://osv.dev/vulnerability/CVE-2026-71852 +Open Source Vulnerabilities. (2026c). *CVE-2026-84309*. Retrieved August 9, + 2026, from https://osv.dev/vulnerability/CVE-2026-84309 -Open Source Vulnerabilities. (2026d). *CVE-2026-71870*. Retrieved August 9, - 2026, from https://osv.dev/vulnerability/CVE-2026-71870 +Open Source Vulnerabilities. (2026d). *CVE-2026-84310*. Retrieved August 9, + 2026, from https://osv.dev/vulnerability/CVE-2026-84310 + +Open Source Vulnerabilities. (2026e). *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/ diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md deleted file mode 100644 index e7ba16b1..00000000 --- a/docs/product-technical-gap-baseline.md +++ /dev/null @@ -1,33 +0,0 @@ -# Product and technical gap baseline - -This document records gaps that are observable from the current NewsDOM code, delivery policy, releases, and open integration work. It is not a roadmap commitment; a gap leaves this list only after the corresponding code and evidence are current on the protected integration path. - -## Product boundary - -NewsDOM API owns the PDF-to-canonical-NewsDOM parsing boundary around MinerU. It accepts an authenticated PDF, validates and bounds the upload, invokes the local MinerU runtime, and returns the repository's NewsDOM response schema. Authentication, parsing and readiness truth stay in this service; consumers such as Naruon use the released API contract rather than source-copying NewsDOM internals. - -The latest immutable GitHub release observed while updating this baseline is `v0.2.0` (published 2026-04-24). `CHANGELOG.md` currently describes an unreleased `0.3.0` migration in which parser authentication becomes default-required. - -## Current gaps - -| Gap | Current evidence | Acceptance | -| --- | --- | --- | -| Interactive API documentation security and behavior | PR #775 repairs unconditional Swagger authorization persistence and the global CSP that blocked FastAPI's generated documentation assets. Authorization persistence is development-only; Swagger and ReDoc now receive route-scoped CSP policies in both runtime profiles, the external Swagger validator is disabled, and non-document API responses retain the locked-down CSP. Repository policy still requires a live localhost `/docs` and `/redoc` smoke for documentation changes. | Current-head unit/coverage/docs gates pass; a real browser/local smoke proves `/docs` and `/redoc` render without CSP errors in production and development; a refresh in development preserves Swagger authorization while production does not persist it; screenshots or equivalent browser evidence correspond to the exact tested head. | -| `0.3.0` release readiness | The changelog still uses an unreleased image placeholder for the authentication migration. The last immutable release is `v0.2.0`. | Protected `develop` evidence is GREEN; package, OpenAPI, container image, SBOM/provenance, rollback instructions and release manifest identify the same version and source; the release tag is immutable and reproducible. | -| Runtime configuration boundary | `AGENTS.md` records `NEWSDOM_MINERU_BIN` as the remaining raw-environment deployment knob and requires future secrets/credentials/external endpoints to use the canonical KV/credential-registry pattern. | The executable-path override has an explicit deployment/configuration ADR or is moved behind the adopted configuration boundary; no new runtime secret is read directly from process environment. | -| Real-data parser acceptance | Synthetic fixtures remain suitable for unit tests, but commercial parsing acceptance requires right-cleared representative PDFs and must not publish private/copyrighted inputs. | A private/right-cleared acceptance corpus exercises representative layouts, languages, page counts and failure cases; results are reproducible and published only as non-sensitive metrics/evidence, not source documents. | - -## Current PR #775 traceability - -The repair sequence on branch `jules-11760207665579123715-6bc329bb` is test-first: - -- Swagger RED regression: `31bf14cd6d71e03b0b31320c940c3b79f3d984ca` -- development-only authorization persistence and initial Swagger CSP fix: `6c4a5846761e4c1eee2d968c690589811de875a7` -- corrected CSP test oracle: `623b14fc62262fddb8e0d810e72317908cf76ae5` -- documentation alignment: `bbaed060f1243dc30653c40b2e66c9d93df46474` -- baseline introduction: `281ace5a3df9f23ef9f43d9984b55d3353c768b5` -- ReDoc CSP RED: `282d5d9259b578913c7fbe29fce9415bad3c106f` -- production-route CSP RED expansion: `31d441f67f8f91f072a04bcd5e30b3481256ec0f` -- route-scoped Swagger/ReDoc causal fix: `d9199541d00a1da1cc732af42f6f59a6e2e59cbe` - -GitHub-hosted verification must be evaluated on the final exact head, not on any predecessor listed above. Queued jobs or CodeQL `startup_failure` before source execution are incomplete evidence, not passing gates and not a reason for no-op source churn. diff --git a/pyproject.toml b/pyproject.toml index 7a29144e..c3a38b71 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.0,<7.0", ] [project.optional-dependencies] diff --git a/tests/test_project_metadata.py b/tests/test_project_metadata.py index 324cb086..ca4f4541 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.0,<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, 0) diff --git a/tests/test_pypdf_security_floor.py b/tests/test_pypdf_security_floor.py index 6a641e83..5120052a 100644 --- a/tests/test_pypdf_security_floor.py +++ b/tests/test_pypdf_security_floor.py @@ -6,9 +6,9 @@ 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, 0) +_CURRENT_PYPDF_CVES = ("CVE-2026-84309", "CVE-2026-84310", "CVE-2026-84311") +_LOCKED_PYPDF_REQUIREMENT = '{ name = "pypdf", specifier = ">=6.16.0,<7.0" },' def _locked_pypdf_version() -> tuple[int, ...]: @@ -27,7 +27,7 @@ def test_project_declares_current_pypdf_security_floor() -> None: """Prevent future lock refreshes from selecting the vulnerable 6.14.x line.""" project_text = Path("pyproject.toml").read_text(encoding="utf-8") - assert '"pypdf>=6.15.0,<7.0"' in project_text + assert '"pypdf>=6.16.0,<7.0"' in project_text def test_lock_uses_current_pypdf_security_release() -> None: @@ -61,7 +61,7 @@ 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 + assert "`pypdf>=6.16.0,<7.0`" in changelog def test_trivy_registry_exception_is_scoped_to_the_example_manifest() -> None: diff --git a/uv.lock b/uv.lock index a0d133b8..5cd42560 100644 --- a/uv.lock +++ b/uv.lock @@ -303,7 +303,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -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.0,<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]] From e8a8375715bffdc80cae6c896d6f0f17cebd9138 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:44:39 +0000 Subject: [PATCH 19/24] =?UTF-8?q?CodeQL=20CI=20=EC=9E=AC=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EA=B1=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=B9=88=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From ad0616dbde4b39059f954aa9ec2ad3187010a3ad Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:24:39 +0000 Subject: [PATCH 20/24] =?UTF-8?q?CodeQL=20CI=20=EC=9E=AC=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EA=B1=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=B9=88=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From af7386e11901f1da35fcd90dc1af4a8becd4c02e Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:12:10 +0000 Subject: [PATCH 21/24] =?UTF-8?q?CodeQL=20CI=20=EC=9E=AC=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EA=B1=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=B9=88=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 75f895e590392805af2d445829742dc7d5775ddf Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:23:27 +0000 Subject: [PATCH 22/24] =?UTF-8?q?Strix=20CI=20=EC=9E=AC=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EA=B1=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=B9=88=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From b26d998e5cf02948fdad723ef376de379209b613 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 04:01:59 +0000 Subject: [PATCH 23/24] =?UTF-8?q?Noema=20CI=20=EC=9E=AC=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EA=B1=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=B9=88=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 4d701f9ac06db11593b88dcd7b3c88851f8b3582 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 05:37:20 +0000 Subject: [PATCH 24/24] =?UTF-8?q?Noema=20CI=20=EC=9E=AC=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EA=B1=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=B9=88=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit