Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
1def61b
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 7, 2026
651c144
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 7, 2026
81d3772
repair(security): separate parser dependency remediation from form vaโ€ฆ
seonghobae Sep 7, 2026
474e733
test(security): require lock beyond current pypdf CVE floor
seonghobae Sep 7, 2026
be1e5c2
docs(security): trace current pypdf CVEs to lock acceptance
seonghobae Sep 7, 2026
7946c14
test(parse): remove misleading multipart-memory regression
seonghobae Sep 7, 2026
e01e0d5
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 7, 2026
b989eab
test: restore complete pypdf security floor contract
seonghobae Sep 8, 2026
3725a01
fix: restore dependency security repair scope
seonghobae Sep 8, 2026
01bf692
docs: align changelog with locked pypdf security floor
seonghobae Sep 8, 2026
82a4c18
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 8, 2026
1178fd8
test(deps): RED enforce patched pypdf security floor
seonghobae Sep 8, 2026
7cfcae3
test(deps): RED reject pypdf 6.16.0 advisories
seonghobae Sep 8, 2026
0062273
repair(deps): remove unrelated form-limit regression
seonghobae Sep 8, 2026
44bb1c4
test(deps): align metadata regression with pypdf 6.16.1
seonghobae Sep 8, 2026
8bba2bf
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 8, 2026
6a77d14
docs(deps): record pypdf 6.16.1 advisory floor
seonghobae Sep 8, 2026
c4e8fa8
docs(deps): correct pypdf advisory and SSDF traceability
seonghobae Sep 8, 2026
2e2564c
repair(deps): preserve lock fix and drop unrelated form drift
seonghobae Sep 8, 2026
d752710
docs(changelog): keep dependency change narrowly scoped
seonghobae Sep 8, 2026
f6f0235
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 8, 2026
ab51446
repair: drop unrelated multipart Form/OOM delta
seonghobae Sep 8, 2026
74eff27
test(deps): enforce complete pypdf 6.16.1 security floor
seonghobae Sep 8, 2026
6d51b4f
docs(deps): align pypdf floor and live SSDF status
seonghobae Sep 8, 2026
9412637
docs(changelog): record complete pypdf advisory floor
seonghobae Sep 8, 2026
175e0c2
docs: establish pypdf security gap baseline
seonghobae Sep 8, 2026
f0f47ac
test(deps): trace current CVE and GHSA identities
seonghobae Sep 8, 2026
fd5c09d
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 8, 2026
9d8452d
repair(deps): remove unrelated form-boundary delta
seonghobae Sep 10, 2026
f43dd09
repair(deps): remove form-boundary test and doctrine contamination
seonghobae Sep 10, 2026
1f9758b
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 10, 2026
8595855
repair(deps): restore canonical security baseline and owner scope
seonghobae Sep 11, 2026
a692541
docs(gap): consolidate current parser and dependency gaps
seonghobae Sep 11, 2026
41f9f74
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 11, 2026
ac0129a
repair(deps): restore canonical pypdf security owner tree
seonghobae Sep 11, 2026
57c3e42
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 11, 2026
d9d48ec
repair(deps): re-adopt canonical pypdf security tree
seonghobae Sep 12, 2026
c6153f7
restack(deps): adopt protected develop after #759
seonghobae Sep 12, 2026
5b4f05c
test(deps): require fixed httpx2 family security floor
seonghobae Sep 12, 2026
836a7ee
fix(deps): raise httpx2 family above current security floors
seonghobae Sep 12, 2026
07ca61a
docs(security): add httpx2 family advisory floor
seonghobae Sep 12, 2026
0a888da
docs(changelog): record httpx2 family security floor
seonghobae Sep 12, 2026
21c8d42
docs(gap): make dependency-security lock atomic
seonghobae Sep 12, 2026
4c84dac
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 12, 2026
8ac1a54
repair(deps): re-adopt atomic dependency-security tree
seonghobae Sep 12, 2026
661f8fa
test(deps): align metadata pypdf floor with advisory boundary
seonghobae Sep 12, 2026
223c406
test(deps): harden pypdf advisory traceability contract
seonghobae Sep 12, 2026
ce66ad0
docs(changelog): trace pypdf advisory identifiers
seonghobae Sep 12, 2026
2ce2b44
๋นˆ ์ปค๋ฐ‹์œผ๋กœ CI ์žฌํŠธ๋ฆฌ๊ฑฐ
seonghobae Sep 19, 2026
63beccd
fix(security): restore dependency authority after false empty replay
seonghobae Sep 19, 2026
bfae71c
๋นˆ ์ปค๋ฐ‹์œผ๋กœ CI ์žฌํŠธ๋ฆฌ๊ฑฐ
seonghobae Sep 19, 2026
80a31bf
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] ์ข…์†์„ฑ ์—…๋ฐ์ดํŠธ
seonghobae Sep 19, 2026
cc0e965
fix(security): restore dependency authority after false empty replay
seonghobae Sep 19, 2026
dd13288
test(security): require patched AnyIO and current transport lock
seonghobae Sep 19, 2026
90010a3
fix(security): integrate AnyIO and transport supplier in dependency oโ€ฆ
seonghobae Sep 19, 2026
8164d97
๋นˆ ์ปค๋ฐ‹์œผ๋กœ CI ์žฌํŠธ๋ฆฌ๊ฑฐ
seonghobae Sep 19, 2026
124e983
๋นˆ ์ปค๋ฐ‹์œผ๋กœ CI ์žฌํŠธ๋ฆฌ๊ฑฐ
seonghobae Sep 20, 2026
ef0afec
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์  (Empty commit for CI re-tโ€ฆ
seonghobae Sep 24, 2026
f5035e8
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์  (Empty commit for CI re-tโ€ฆ
seonghobae Sep 24, 2026
6f0f171
fix(deps): restore dependency owner after form-field drift
seonghobae Sep 24, 2026
072ea5d
chore(deps): keep retrospective doctrine out of dependency owner
seonghobae Sep 24, 2026
4cb0841
docs(deps): align transport lock evidence with exact lock
seonghobae Sep 24, 2026
dcda6f3
fix(deps): keep AnyIO on compatible patched series
seonghobae Sep 26, 2026
1217522
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 26, 2026
e5fad7b
test(security): derive pypdf floor assertions
seonghobae Sep 26, 2026
1587c55
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 26, 2026
d50c763
repair(owner): restore dependency-security contract after inverse replay
seonghobae Sep 26, 2026
421293b
test(security): preserve pypdf floor authority after replay
seonghobae Sep 26, 2026
33d02bb
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 26, 2026
22fe7ff
repair(owner): restore dependency contract after repeated inverse replay
seonghobae Sep 26, 2026
0acb148
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 26, 2026
5afb7ea
test(security): preserve owner and pin form-field length RED
seonghobae Sep 26, 2026
dd4031b
fix(api): bound validated parser selector fields
seonghobae Sep 26, 2026
97735d1
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 26, 2026
62837de
repair(owner): preserve security and form contracts after inverse replay
seonghobae Sep 26, 2026
18e41f7
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 26, 2026
404ae46
fix(security): restore strict form-field and dependency contracts
seonghobae Sep 26, 2026
450ae53
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 26, 2026
3e4d204
fix(security): restore strict form and dependency contracts again
seonghobae Sep 26, 2026
8732f87
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 26, 2026
58afaf0
fix(security): restore form and dependency contracts after replay
seonghobae Sep 26, 2026
704078d
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 26, 2026
927ff2d
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 26, 2026
23e4815
๐Ÿ›ก๏ธ Sentinel: [MEDIUM] Fix ํผ ํ•„๋“œ ๊ธธ์ด ์ œํ•œ ๋ˆ„๋ฝ ์ทจ์•ฝ์ 
seonghobae Sep 30, 2026
239648e
feat: add Permissions-Policy security header
seonghobae Sep 30, 2026
e24a5bc
feat: add Permissions-Policy security header
seonghobae Sep 30, 2026
50a314b
feat: add Permissions-Policy security header
seonghobae Sep 30, 2026
2dc876b
merge: carry dependency security owner into PR #987
seonghobae Sep 30, 2026
560d06d
feat: add Permissions-Policy security header
seonghobae Sep 30, 2026
420a423
fix(security): restore dependency owner after revert commit
seonghobae Sep 30, 2026
5a8529b
feat: add Permissions-Policy security header
seonghobae Sep 30, 2026
64f00ae
feat: add Permissions-Policy security header
seonghobae Sep 30, 2026
a0c033d
feat: add Permissions-Policy security header
seonghobae Oct 1, 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
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,8 @@
**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.

## 2024-11-20 - Add Permissions-Policy security header
**Vulnerability:** Missing `Permissions-Policy` header allows potential abuse of sensitive browser features (e.g. geolocation, camera, microphone) if the API response is rendered or framed in a browser context.
**Learning:** Adding a restrictive `Permissions-Policy` header provides defense in depth by explicitly disabling unnecessary sensitive browser features for the API response.
**Prevention:** Always include `Permissions-Policy: geolocation=(), camera=(), microphone=()` along with other standard security headers like `Content-Security-Policy` and `X-Frame-Options`.
1 change: 1 addition & 0 deletions src/newsdom_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def _apply_security_headers(response: Response, request: Request) -> Response:
)
response.headers["Referrer-Policy"] = "no-referrer"
response.headers["Cache-Control"] = "no-store, no-cache, max-age=0"
response.headers["Permissions-Policy"] = "geolocation=(), camera=(), microphone=()"
forwarded_proto = request.headers.get("x-forwarded-proto", "")
is_https = request.url.scheme == "https" or forwarded_proto.lower() == "https"
if is_https:
Expand Down
4 changes: 3 additions & 1 deletion src/newsdom_api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ class ArticleNode(BaseModel):
body_blocks: List[str] = Field(
default_factory=list,
description="Ordered text blocks that make up the article body.",
json_schema_extra={"example": ["First paragraph of the article.", "Second paragraph."]},
json_schema_extra={
"example": ["First paragraph of the article.", "Second paragraph."]
},
)
images: List[ImageNode] = Field(
default_factory=list,
Expand Down
27 changes: 16 additions & 11 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@
security_boundary_middleware,
)

_PDF_FILES = {
"file": ("fixture.pdf", b"%PDF-1.4\n%synthetic\n", "application/pdf")
}
_PDF_FILES = {"file": ("fixture.pdf", b"%PDF-1.4\n%synthetic\n", "application/pdf")}


def _settings(
Expand Down Expand Up @@ -147,9 +145,12 @@ def test_development_disabled_mode_allows_parse_and_warns_once(
assert client.post("/parse", files=_PDF_FILES).status_code == 200
assert parser_spy["count"] == 2
messages = [record.getMessage() for record in caplog.records]
assert messages.count(
"Parser authentication is disabled for the explicit development profile"
) == 1
assert (
messages.count(
"Parser authentication is disabled for the explicit development profile"
)
== 1
)


def test_direct_runtime_settings_reject_invalid_security_invariants() -> None:
Expand Down Expand Up @@ -350,11 +351,15 @@ def test_concurrent_requests_cannot_switch_authentication_state(
)

def request(token: str) -> int:
return TestClient(application).post(
"/parse",
files=_PDF_FILES,
headers={"Authorization": f"Bearer {token}"},
).status_code
return (
TestClient(application)
.post(
"/parse",
files=_PDF_FILES,
headers={"Authorization": f"Bearer {token}"},
)
.status_code
)

tokens = ["fixed" if index % 2 == 0 else "changed" for index in range(20)]
with ThreadPoolExecutor(max_workers=8) as executor:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_auth_deployment_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def _project_version(pyproject_text: str) -> str:
)
match = (
re.search(
r'''^version\s*=\s*(["'])([^"']+)\1\s*(?:#.*)?$''',
r"""^version\s*=\s*(["'])([^"']+)\1\s*(?:#.*)?$""",
project_table.group("body"),
re.MULTILINE,
)
Expand Down
28 changes: 8 additions & 20 deletions tests/test_auth_fail_closed_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
)
from newsdom_api.main import create_app

_PDF_FILES = {
"file": ("fixture.pdf", b"%PDF-1.4\n%synthetic\n", "application/pdf")
}
_PDF_FILES = {"file": ("fixture.pdf", b"%PDF-1.4\n%synthetic\n", "application/pdf")}


def test_default_configuration_without_token_blocks_parser_before_work(
Expand All @@ -36,12 +34,10 @@ def fake_parse_pdf(*_args, **_kwargs):
monkeypatch.setattr("newsdom_api.main._validate_pdf_structure", lambda _: None)
monkeypatch.setattr("newsdom_api.main.parse_pdf", fake_parse_pdf)

application = create_app(
settings, runtime_readiness_probe=lambda: True
application = create_app(settings, runtime_readiness_probe=lambda: True)
response = TestClient(application, raise_server_exceptions=False).post(
"/parse", files=_PDF_FILES
)
response = TestClient(
application, raise_server_exceptions=False
).post("/parse", files=_PDF_FILES)

assert response.status_code == 503
assert response.json() == {"detail": "Service Unavailable"}
Expand All @@ -58,13 +54,9 @@ def test_ready_fails_closed_when_required_authentication_is_unconfigured(
runtime_profile=RuntimeProfile.PRODUCTION,
api_token=None,
)
application = create_app(
settings, runtime_readiness_probe=lambda: True
)
application = create_app(settings, runtime_readiness_probe=lambda: True)

response = TestClient(
application, raise_server_exceptions=False
).get("/ready")
response = TestClient(application, raise_server_exceptions=False).get("/ready")

assert response.status_code == 503
assert response.json() == {"detail": "Service Unavailable"}
Expand All @@ -82,13 +74,9 @@ def test_health_remains_liveness_only_when_authentication_is_unconfigured(
runtime_profile=RuntimeProfile.PRODUCTION,
api_token=None,
)
application = create_app(
settings, runtime_readiness_probe=lambda: False
)
application = create_app(settings, runtime_readiness_probe=lambda: False)

response = TestClient(
application, raise_server_exceptions=False
).get("/health")
response = TestClient(application, raise_server_exceptions=False).get("/health")

assert response.status_code == 200
assert response.json() == {"status": "ok"}
4 changes: 1 addition & 3 deletions tests/test_auth_protocol_edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
)
from newsdom_api.main import create_app

_PDF_FILES = {
"file": ("fixture.pdf", b"%PDF-1.4\n%synthetic\n", "application/pdf")
}
_PDF_FILES = {"file": ("fixture.pdf", b"%PDF-1.4\n%synthetic\n", "application/pdf")}
_BEARER_PREFIX = "Bearer "


Expand Down
4 changes: 4 additions & 0 deletions tests/test_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ def test_healthcheck():
response.headers.get("Strict-Transport-Security")
== "max-age=31536000; includeSubDomains"
)
assert (
response.headers.get("Permissions-Policy")
== "geolocation=(), camera=(), microphone=()"
)


def test_healthcheck_omits_hsts_for_plain_http():
Expand Down
4 changes: 4 additions & 0 deletions tests/test_parse_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,10 @@ def fake_parse_pdf_bytes(file_path, filename, **kwargs):
)
assert response.headers.get("Referrer-Policy") == "no-referrer"
assert response.headers.get("Cache-Control") == "no-store, no-cache, max-age=0"
assert (
response.headers.get("Permissions-Policy")
== "geolocation=(), camera=(), microphone=()"
)
assert "Strict-Transport-Security" not in response.headers


Expand Down
6 changes: 2 additions & 4 deletions tests/test_project_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,10 @@ def test_project_declares_python_compatible_locked_fuzz_extra():

assert "fuzz = [" in text
assert (
'"atheris==3.0.0 ; platform_system == \'Linux\' and '
'python_version == \'3.11\'"'
"\"atheris==3.0.0 ; platform_system == 'Linux' and python_version == '3.11'\""
) in text
assert (
'"atheris==3.1.0 ; platform_system == \'Linux\' and '
'python_version >= \'3.12\'"'
"\"atheris==3.1.0 ; platform_system == 'Linux' and python_version >= '3.12'\""
) in text
assert _locked_package_versions("atheris") == {(3, 0, 0), (3, 1, 0)}
assert '"pyinstaller==6.21.0"' in text
Expand Down
4 changes: 1 addition & 3 deletions tests/test_pypdf_security_floor.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,7 @@ def test_trivy_registry_exception_is_scoped_to_the_example_manifest() -> None:
ignore_document = yaml.safe_load(
Path(".trivyignore.yaml").read_text(encoding="utf-8")
)
exceptions = {
entry["id"]: entry for entry in ignore_document["misconfigurations"]
}
exceptions = {entry["id"]: entry for entry in ignore_document["misconfigurations"]}

assert trivy_config["ignorefile"] == ".trivyignore.yaml"
assert exceptions["KSV-0125"]["paths"] == [
Expand Down
Loading