Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
78b4040
🛡️ Sentinel: FastAPI 폼 필드 입력 제한 추가로 메모리 고갈 취약점 완화
seonghobae Sep 2, 2026
03f2ea6
🛡️ Sentinel: MEDIUM Fix pypdf vulnerabilities
seonghobae Sep 3, 2026
4e2d407
test: reproduce pre-parser multipart body limit gap
seonghobae Sep 3, 2026
7cf7813
fix: bound parse request body before multipart parsing
seonghobae Sep 3, 2026
86253b9
test: keep request-body limit regression lint-clean
seonghobae Sep 3, 2026
51d1cff
opencode-agent 판정 대기
seonghobae Sep 3, 2026
b098e7e
repair: preserve pre-parser request-body admission contract
seonghobae Sep 3, 2026
538c69d
docs: align pypdf security baseline with 6.16.2
seonghobae Sep 3, 2026
d52f68f
docs: consolidate unreleased security notes
seonghobae Sep 3, 2026
77e59fb
opencode-agent 판정 대기
seonghobae Sep 3, 2026
5576480
fix: preserve validated request admission repair after intervening delta
seonghobae Sep 3, 2026
cbfd7b8
test(isolation): reject global dependency override clearing
seonghobae Sep 3, 2026
db07e99
opencode-agent 판정 대기 (테스트 수정 적용)
seonghobae Sep 3, 2026
e2a22a7
docs(changelog): remove duplicate stale Unreleased block
seonghobae Sep 3, 2026
fe16320
opencode-agent 판정 대기
seonghobae Sep 3, 2026
e9a97e8
opencode-agent 판정 대기
seonghobae Sep 3, 2026
c81c002
opencode-agent 판정 대기
seonghobae Sep 3, 2026
b6ad399
opencode-agent 판정 대기
seonghobae Sep 4, 2026
fa12eab
docs(changelog): remove reintroduced duplicate Unreleased block
seonghobae Sep 4, 2026
d45a0b3
repair(changelog): preserve canonical wording after duplicate removal
seonghobae Sep 4, 2026
3b51957
CI 재트리거를 위한 빈 커밋
seonghobae Sep 4, 2026
78a931e
repair(sentinel): restore canonical security journal
seonghobae Sep 4, 2026
977ff0c
repair(changelog): keep one canonical unreleased section
seonghobae Sep 4, 2026
0be5ba2
test(security): track current pypdf advisories
seonghobae Sep 4, 2026
0877c63
docs(security): trace current pypdf advisories
seonghobae Sep 4, 2026
c77a6fe
opencode-agent 판정 대기
seonghobae Sep 4, 2026
adc2a7e
docs(security): restore canonical Sentinel history
seonghobae Sep 4, 2026
7191cb2
docs(security): make Sentinel tree match protected base
seonghobae Sep 4, 2026
eaca4fe
docs(changelog): remove duplicate unreleased security block
seonghobae Sep 4, 2026
a03ef65
test: preserve sanitized form-boundary contract
seonghobae Sep 5, 2026
3f011a2
test: align form-boundary regression with FastAPI contract
seonghobae Sep 5, 2026
5061bfa
docs: refresh pypdf advisory traceability
seonghobae Sep 5, 2026
ebd6c71
test: track current pypdf advisory set
seonghobae Sep 5, 2026
d148e37
테스트 실패 오류 수정 (FastAPI의 422 폼 최대길이 에러 응답 검증 형식 오류 해결)
seonghobae Sep 5, 2026
17d9a8b
CI 재트리거를 위한 커밋 수정
seonghobae Sep 5, 2026
866c40f
CI 재트리거를 위한 커밋 수정
seonghobae Sep 5, 2026
bb1c81e
CI 재트리거를 위한 빈 커밋
seonghobae Sep 5, 2026
b36227d
CI 재트리거를 위한 빈 커밋
seonghobae Sep 6, 2026
e72688e
CI 재트리거를 위한 빈 커밋
seonghobae Sep 6, 2026
6e912c9
CI 재트리거를 위한 빈 커밋
seonghobae Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,13 @@
**Vulnerability:** The `_safe_upload_filename` function used `filename.replace`, `PurePosixPath`, and `re.sub` on unbounded client input, making it vulnerable to ReDoS or CPU/memory exhaustion (DoS) when fed extremely long strings.
**Learning:** Even fast standard library functions like `PurePosixPath` and string replacements can cause significant lag when chained on strings in the megabytes. String processing operations should always bound their inputs first if the input is untrusted and can be arbitrarily large.
**Prevention:** Cap the length of client-provided filename strings early by slicing them (e.g. `filename = filename[-512:]`) before doing more complex string parsing or regex replacements, especially when only the basename suffix is relevant.

## 2026-09-02 - Prevent Memory Exhaustion via Unbounded Form Fields
**Vulnerability:** Textual `Form` fields in FastAPI (`language`, `mode`) lacked `max_length` limits, which could allow memory exhaustion via large payloads since `python-multipart` loads form data into memory before routing.
**Learning:** Even though payload size limits might exist for uploaded files, missing constraints on simple form fields allow attackers to send massive strings in multipart payloads, causing memory bloat (DoS).
**Prevention:** Always set explicit `max_length` attributes on `Form()` fields in FastAPI endpoints.

## 2026-09-03 - Upgrade pypdf to 6.16.2 to resolve CVEs
**Vulnerability:** pypdf versions below 6.16.2 contain security vulnerabilities flagged by trivy-fs (CVE-2026-84309, CVE-2026-84310, CVE-2026-84311).
**Learning:** When updating the `pypdf` dependency to address security vulnerabilities from `trivy-fs` CI failures, version assertions in `tests/test_project_metadata.py` and the specific constants (`_REQUIRED_PYPDF_VERSION`, `_LOCKED_PYPDF_REQUIREMENT`) in `tests/test_pypdf_security_floor.py` must be updated to match the new version to prevent test failures.
**Prevention:** Regularly scan dependencies with trivy and upgrade vulnerable packages, updating hardcoded test assertions accordingly.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [CLI] 파싱된 NewsDOM JSON에서 순수 텍스트 데이터를 추출하여 텍스트 파일 또는 stdout으로 출력하는 `tools/extract_text.py` 도구를 추가했습니다.

### Security
- `/parse` POST 요청에 multipart parser 이전의 21 MiB request-body admission limit를 추가해 선언된 초과 `Content-Length`는 body read 전에 413으로 거절하고, 길이가 없거나 신뢰할 수 없는 stream은 실제 누적 receive bytes 기준으로 제한합니다. 기존 PDF payload 상한 20 MiB와 multipart/form framing 예산 1 MiB를 분리해 유지합니다.
- `/parse`의 `language` 및 `mode` Form 필드에 `max_length=50` 제한을 추가했습니다. 이 검증은 parser 이후의 필드 경계이며, pre-parser request-body limit를 대체하지 않습니다.
- `/parse` authentication is now immutable per application instance and fails closed before multipart body parsing when required configuration is missing. Hostile missing, invalid, Unicode, oversized, and duplicated Authorization headers return one non-sensitive response.
- Added unauthenticated `/ready` traffic readiness that combines authentication configuration with MinerU executable availability while `/health` remains liveness-only.
- Hardened the Kubernetes deployment example with a restricted namespace policy, explicit non-root UID/GID, `RuntimeDefault` seccomp, disabled privilege escalation, dropped Linux capabilities, a read-only root filesystem, and bounded writable runtime volumes.
- 전역 500 에러 응답에도 표준 보안 헤더를 적용하여 예외 경로에서 header 누락을 방지
- 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` findings를 해소: 런타임 경로의 `pillow` 12.2.0→12.3.0 (PYSEC-2026-3451/3452/3453/3454/3493/3494/3495/3496, 이미지 파서 취약점 8건), `pypdf>=6.16.2,<7.0` (lock 6.16.2; 기존 CVE-2026-59935/59936/59937/59938/71852/71870뿐 아니라 upstream의 6.16.0/6.16.1 patched floors 이후 버전), `click` 8.3.2→8.4.2 (PYSEC-2026-2132). 빌드 도구 `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()` 캐스팅을 제거함으로써 처리 속도를 개선했습니다.
Expand Down
53 changes: 36 additions & 17 deletions docs/doctoring/dependency-security-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,30 @@ 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.2,<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.

## Threat and impact rationale

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
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
therefore move together to 6.15.0 rather than hiding the findings in
`.trivyignore`.
The earlier baseline raised pypdf to 6.15.0 after the repository's Trivy
filesystem gate reported CVE-2026-71852 and CVE-2026-71870 against 6.14.2.
Upstream subsequently published additional pypdf advisories: the
`TreeObject.insert_child` infinite-loop issue (CVE-2026-84309) is fixed in
6.16.0, while outline retrieval (CVE-2026-84310) and XForm extraction
(CVE-2026-84311) resource-consumption issues are fixed in 6.16.1. The current
declaration and lock use 6.16.2, which is newer than each of those patched
floors. This record does not claim that every upstream advisory is reachable
through NewsDOM's current strict `PdfReader` validation path; the floor keeps the
shipped parser dependency outside the upstream affected ranges while repository
tests and scanners determine product-specific acceptance.

CVE-2026-59890 affects setuptools versions before 83.0.0. On
normalization-preserving macOS filesystems, specially named files could bypass
Expand All @@ -43,11 +46,12 @@ is a build-time rather than request-time issue, it can compromise release
contents, so the build-system floor is raised to 83.0.0.

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
release and the artifact hashes recorded in this repository's generated lock.
published cryptographic file digests. PyPI records pypdf 6.16.2 as released on
August 23, 2026 and, as checked on September 5, 2026, as the latest release; its
source and wheel artifacts were uploaded through Trusted Publishing and have
published hashes. 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.

## Secure-development and provenance controls

Expand Down Expand Up @@ -129,14 +133,29 @@ 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

py-pdf. (2026a). *Possible infinite loop for TreeObject.insert_child*
(GHSA-jp53-mhqp-8xcg; CVE-2026-84309). GitHub Security Advisory. Retrieved
September 5, 2026, from
https://github.com/py-pdf/pypdf/security/advisories/GHSA-jp53-mhqp-8xcg

py-pdf. (2026b). *Possible long runtimes/large memory usage when retrieving
outlines* (GHSA-23w6-3w8w-8484; CVE-2026-84310). GitHub Security Advisory.
Retrieved September 5, 2026, from
https://github.com/py-pdf/pypdf/security/advisories/GHSA-23w6-3w8w-8484

py-pdf. (2026c). *Possible long runtimes/large memory usage when extracting
XForm objects* (GHSA-763m-79hh-57f2; CVE-2026-84311). GitHub Security
Advisory. Retrieved September 5, 2026, from
https://github.com/py-pdf/pypdf/security/advisories/GHSA-763m-79hh-57f2

Python Packaging Authority. (2026a). *Digital attestations*. PyPI Docs.
Retrieved August 4, 2026, from https://docs.pypi.org/attestations/

Python Packaging Authority. (2026b). *Pillow 12.3.0*. Python Package Index.
Retrieved August 4, 2026, from https://pypi.org/project/pillow/12.3.0/

Python Packaging Authority. (2026c). *pypdf 6.15.0*. Python Package Index.
Retrieved August 9, 2026, from https://pypi.org/project/pypdf/6.15.0/
Python Packaging Authority. (2026c). *pypdf 6.16.2*. Python Package Index.
Retrieved September 5, 2026, from https://pypi.org/project/pypdf/6.16.2/

Python Packaging Authority. (2026d). *setuptools 83.0.0*. Python Package Index.
Retrieved August 4, 2026, from https://pypi.org/project/setuptools/83.0.0/
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.2,<7.0",
Comment thread
seonghobae marked this conversation as resolved.
]

[project.optional-dependencies]
Expand Down
95 changes: 95 additions & 0 deletions src/newsdom_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from fastapi.security import HTTPBearer
from pypdf import PdfReader
from pypdf.errors import PdfReadError
from starlette.types import ASGIApp, Message, Receive, Scope, Send

from .config import (
AuthenticationMode,
Expand All @@ -42,6 +43,7 @@
from .service import parse_pdf

MAX_PARSE_UPLOAD_BYTES = 20 * 1024 * 1024
MAX_PARSE_REQUEST_BYTES = MAX_PARSE_UPLOAD_BYTES + (1024 * 1024)
MAX_AUTHORIZATION_HEADER_BYTES = MAX_BEARER_HEADER_BYTES
UNSUPPORTED_MEDIA_DETAIL = "Unsupported Media Type"
PAYLOAD_TOO_LARGE_DETAIL = "Payload Too Large"
Expand All @@ -60,6 +62,92 @@
]


class _RequestBodyTooLarge(Exception):
"""Signal that a streamed request crossed its pre-parser byte budget."""


class RequestBodyLimitMiddleware:
"""Bound one HTTP request path before multipart or endpoint parsing begins."""

def __init__(
self,
app: ASGIApp,
*,
max_body_bytes: int,
path: str,
) -> None:
"""Configure an exact positive body budget for one request path."""

if max_body_bytes < 1:
raise ValueError("max_body_bytes must be positive")
if not path.startswith("/"):
raise ValueError("path must be absolute")
self.app = app
self.max_body_bytes = max_body_bytes
self.path = path

@staticmethod
def _declared_content_length(scope: Scope) -> int | None:
"""Return one trustworthy non-negative Content-Length, if present."""

values = [
value
for name, value in scope.get("headers", [])
if name.lower() == b"content-length"
]
if len(values) != 1:
return None
value = values[0]
if not value.isdigit() or len(value) > 20:
return None
return int(value)

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
"""Reject declared or streamed oversized bodies before downstream parsing."""

if (
scope["type"] != "http"
or scope.get("method") != "POST"
or scope.get("path") != self.path
):
await self.app(scope, receive, send)
return

declared_length = self._declared_content_length(scope)
if (
declared_length is not None
and declared_length > self.max_body_bytes
):
response = JSONResponse(
status_code=413,
content={"detail": PAYLOAD_TOO_LARGE_DETAIL},
)
await response(scope, receive, send)
return

bytes_received = 0

async def limited_receive() -> Message:
"""Count streamed request bytes before exposing them downstream."""

nonlocal bytes_received
message = await receive()
if message["type"] == "http.request":
bytes_received += len(message.get("body", b""))
if bytes_received > self.max_body_bytes:
raise _RequestBodyTooLarge
return message

try:
await self.app(scope, limited_receive, send)
except _RequestBodyTooLarge:
response = JSONResponse(
status_code=413,
content={"detail": PAYLOAD_TOO_LARGE_DETAIL},
)
await response(scope, receive, send)


def _apply_security_headers(response: Response, request: Request) -> Response:
"""Inject standard security headers into an API response."""

Expand Down Expand Up @@ -205,6 +293,7 @@ async def parse(
language: Annotated[
str,
Form(
max_length=50,
Comment thread
seonghobae marked this conversation as resolved.
description=(
"MinerU language family or compatibility alias (e.g. `ch`, "
"`en`, `japan`, `korean`, `arabic`, `devanagari`)."
Expand All @@ -214,6 +303,7 @@ async def parse(
mode: Annotated[
str,
Form(
max_length=50,
description=(
"MinerU parsing mode: `auto` (born-digital text PDFs skip forced "
"OCR), `ocr` (force OCR), or `txt` (embedded text layer only)."
Expand Down Expand Up @@ -327,6 +417,11 @@ def create_app(
application.state.runtime_readiness_probe = (
runtime_readiness_probe or mineru_runtime_available
)
application.add_middleware(
RequestBodyLimitMiddleware,
max_body_bytes=MAX_PARSE_REQUEST_BYTES,
path="/parse",
)
application.middleware("http")(security_boundary_middleware)
application.add_exception_handler(Exception, global_exception_handler)
application.add_api_route(
Expand Down
38 changes: 38 additions & 0 deletions tests/test_parse_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,3 +555,41 @@ def spy_unlink(self, missing_ok=False):
# We should have unlinked exactly one file, which should be in the temp directory
assert len(unlinked_paths) == 1
assert "tmp" in unlinked_paths[0].lower() or "temp" in unlinked_paths[0].lower()

def test_parse_form_field_max_length_exceeded(monkeypatch):
"""Test that Form fields reject inputs longer than max_length=50."""
# Temporarily override runtime settings to bypass authentication in tests
from newsdom_api.config import AuthenticationMode, RuntimeSettings, RuntimeProfile
from newsdom_api.main import _runtime_settings

monkeypatch.setitem(
app.dependency_overrides,
_runtime_settings,
lambda request: RuntimeSettings(
authentication_mode=AuthenticationMode.DISABLED,
runtime_profile=RuntimeProfile.DEVELOPMENT
)
)

# We must patch the access failure validation to bypass authentication during test
monkeypatch.setattr("newsdom_api.main._parse_access_failure", lambda request: None)

client = TestClient(app, raise_server_exceptions=False)

long_string = "a" * 51

response = client.post(
"/parse",
files={"file": ("dummy.pdf", b"%PDF-dummy", "application/pdf")},
data={"language": long_string, "mode": "auto"},
)
assert response.status_code == 422
assert "language" in response.text

response = client.post(
"/parse",
files={"file": ("dummy.pdf", b"%PDF-dummy", "application/pdf")},
data={"language": "ch", "mode": long_string},
)
assert response.status_code == 422
assert "mode" in response.text
54 changes: 54 additions & 0 deletions tests/test_parse_endpoint_max_length.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Regression tests for bounded `/parse` form values."""

import pytest
from fastapi.testclient import TestClient

from newsdom_api.config import AuthenticationMode, RuntimeSettings
from newsdom_api.main import _runtime_settings, app


_MINIMAL_PDF = b"%PDF-1.4\n%%EOF"
_MISSING = object()


@pytest.fixture
def no_auth_client():
"""Disable authentication without clearing unrelated dependency overrides."""
previous = app.dependency_overrides.get(_runtime_settings, _MISSING)
app.dependency_overrides[_runtime_settings] = lambda: RuntimeSettings(
authentication_mode=AuthenticationMode.DISABLED
)
try:
with TestClient(app) as client:
yield client
finally:
if previous is _MISSING:
app.dependency_overrides.pop(_runtime_settings, None)
else:
app.dependency_overrides[_runtime_settings] = previous


@pytest.mark.parametrize(
("bounded_field", "language", "mode"),
(("language", "a" * 51, "auto"), ("mode", "ch", "b" * 51)),
)
def test_parse_endpoint_rejects_overlong_form_values(
no_auth_client: TestClient,
bounded_field: str,
language: str,
mode: str,
) -> None:
"""Reject each overlong field through FastAPI's declared form-value contract."""
response = no_auth_client.post(
"/parse",
files={"file": ("fixture.pdf", _MINIMAL_PDF, "application/pdf")},
data={"language": language, "mode": mode},
)

assert response.status_code == 422
detail = response.json()["detail"]
assert any(
error.get("loc") == ["body", bounded_field]
and error.get("type") == "string_too_long"
for error in detail
)
2 changes: 1 addition & 1 deletion tests/test_project_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.2,<7.0"' in dependencies_section
assert 'requires = ["setuptools>=83", "wheel"]' in text


Expand Down
Loading
Loading