Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a3421f3
fix(deps): bump next to 16.3.4 and sharp override to 0.35.4
seonghobae Sep 9, 2026
d76f7b4
test(security): lock frontend patched dependency floors
seonghobae Sep 9, 2026
a5abe04
test(deps): reproduce weak frontend lock validation
seonghobae Sep 9, 2026
e8a54fc
fix(deps): validate frontend lock security floors structurally
seonghobae Sep 9, 2026
b97f42f
fix(deps): raise frontend audit security floors
seonghobae Sep 9, 2026
09cb87a
test(deps): bind js-yaml owner succession structurally
seonghobae Sep 9, 2026
a6715c9
test(deps): reproduce missing Vitest lock-entry gap
seonghobae Sep 9, 2026
21897d8
test(deps): require Vitest lock resolutions
seonghobae Sep 9, 2026
15fecaa
test(security): reproduce Vitest importer drift
seonghobae Sep 9, 2026
17a7618
test(security): bind Vitest importer contract
seonghobae Sep 9, 2026
5487141
fix(deps): bind Vitest importer package records
seonghobae Sep 12, 2026
4e66036
docs(gap): record Vitest lockfile repair evidence
seonghobae Sep 12, 2026
d8327d4
fix(governance): keep dependency PR off canonical gap ledger
seonghobae Sep 12, 2026
8175f7f
test(deps): require patched Nano ID resolution
seonghobae Sep 12, 2026
9d6d1e0
fix(deps): pin patched Nano ID in pnpm owner
seonghobae Sep 12, 2026
ecbeb7a
test(deps): bind PostCSS floor to reviewed manifest
seonghobae Sep 15, 2026
9066c3a
test(deps): reproduce below-floor PostCSS lock drift
seonghobae Sep 16, 2026
ccb18dd
fix(deps): reject stale PostCSS lock resolutions
seonghobae Sep 16, 2026
c2f9480
test(deps): reproduce missing PostCSS importer snapshot
seonghobae Sep 16, 2026
509be4c
fix(deps): bind root PostCSS importer to snapshot
seonghobae Sep 16, 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
166 changes: 166 additions & 0 deletions backend/tests/test_frontend_framework_security_floor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Fail closed when frontend framework/image dependencies regress below patched floors."""

from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any

import pytest
import yaml


REPO_ROOT = Path(__file__).resolve().parents[2]
FRONTEND_ROOT = REPO_ROOT / "frontend"
NEXT_SECURITY_FLOOR = (16, 3, 3)
SHARP_SECURITY_FLOOR = (0, 35, 4)


def _exact_version(value: str) -> tuple[int, int, int]:
"""Return a three-part exact version, rejecting ranges and prereleases."""

match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", value)
assert match is not None, f"expected exact semantic version, got {value!r}"
return tuple(int(part) for part in match.groups())


def _resolved_version(value: str) -> tuple[int, int, int]:
"""Return the exact version prefix from a pnpm peer-qualified resolution."""

version = value.split("(", 1)[0]
return _exact_version(version)


def _package_key_version(package_key: str, package_name: str) -> tuple[int, int, int]:
"""Return the version encoded by one pnpm package/snapshot key."""

prefix = f"{package_name}@"
assert package_key.startswith(prefix), (
f"expected {package_name!r} lock key, got {package_key!r}"
)
return _resolved_version(package_key[len(prefix) :])


def _assert_lock_contract(
lock: dict[str, Any],
next_value: str,
eslint_next_value: str,
sharp_value: str,
) -> None:
"""Validate root resolution identity and every locked Next.js/sharp security floor."""

importer = lock["importers"]["."]
next_import = importer["dependencies"]["next"]
assert next_import["specifier"] == next_value, (
"root importer must preserve the package.json Next.js specifier"
)
assert _resolved_version(str(next_import["version"])) == _exact_version(next_value), (
"root importer must resolve the reviewed Next.js release"
)
assert f"next@{next_import['version']}" in lock["snapshots"], (
"root importer Next.js resolution must reference an existing snapshot"
)

eslint_next_import = importer["devDependencies"]["eslint-config-next"]
assert eslint_next_import["specifier"] == eslint_next_value, (
"root importer must preserve the eslint-config-next specifier"
)
assert _resolved_version(str(eslint_next_import["version"])) == _exact_version(
eslint_next_value
), "root importer must resolve the reviewed eslint-config-next release"
assert f"eslint-config-next@{eslint_next_import['version']}" in lock["snapshots"], (
"root importer eslint-config-next resolution must reference an existing snapshot"
)

assert str(lock["overrides"]["sharp"]) == sharp_value, (
"lockfile sharp override must match the reviewed workspace override"
)

expected_next = _exact_version(next_value)
expected_sharp = _exact_version(sharp_value)
for section_name in ("packages", "snapshots"):
section = lock[section_name]
next_keys = [key for key in section if key.startswith("next@")]
sharp_keys = [key for key in section if key.startswith("sharp@")]

assert next_keys, f"{section_name} must contain a Next.js resolution"
assert sharp_keys, f"{section_name} must contain a sharp resolution"
assert any(
_package_key_version(key, "next") == expected_next for key in next_keys
), f"{section_name} must contain the reviewed Next.js release"
assert any(
_package_key_version(key, "sharp") == expected_sharp for key in sharp_keys
), f"{section_name} must contain the reviewed sharp release"

for package_key in next_keys:
assert _package_key_version(package_key, "next") >= NEXT_SECURITY_FLOOR, (
f"{section_name} contains Next.js below the reviewed security floor: "
f"{package_key}"
)
for package_key in sharp_keys:
assert _package_key_version(package_key, "sharp") >= SHARP_SECURITY_FLOOR, (
f"{section_name} contains sharp below the reviewed security floor: "
f"{package_key}"
)


def _frontend_security_inputs() -> tuple[str, str, str, dict[str, Any]]:
"""Load the manifest, workspace override, and generated lock contract."""

package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8"))
next_value = package["dependencies"]["next"]
eslint_next_value = package["devDependencies"]["eslint-config-next"]
workspace = yaml.safe_load(
(FRONTEND_ROOT / "pnpm-workspace.yaml").read_text(encoding="utf-8")
)
sharp_value = str(workspace["overrides"]["sharp"])
lock = yaml.safe_load(
(FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8")
)
return next_value, eslint_next_value, sharp_value, lock


def test_frontend_framework_and_image_security_floors() -> None:
"""Keep manifests and every generated lock resolution at reviewed patched releases."""

next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs()

assert _exact_version(next_value) >= NEXT_SECURITY_FLOOR, (
"Next.js must include the fixes for CVE-2026-75604 and "
"GHSA-2xp9-vwfh-vxw4"
)
assert eslint_next_value == next_value, (
"eslint-config-next must stay on the same reviewed release as Next.js"
)
assert _exact_version(sharp_value) >= SHARP_SECURITY_FLOOR, (
"sharp must include the fix for GHSA-rgj7-g3m4-5g8c"
)
_assert_lock_contract(lock, next_value, eslint_next_value, sharp_value)


@pytest.mark.parametrize("field", ["specifier", "version"])
def test_security_floor_rejects_root_importer_drift(field: str) -> None:
"""Reject a partially regenerated lock whose root Next.js importer drifts."""

next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs()
lock["importers"]["."]["dependencies"]["next"][field] = "16.3.2"

with pytest.raises(AssertionError):
_assert_lock_contract(lock, next_value, eslint_next_value, sharp_value)


@pytest.mark.parametrize(
("section_name", "package_key"),
[("packages", "next@16.3.2"), ("snapshots", "sharp@0.35.3")],
)
def test_security_floor_rejects_every_below_floor_lock_entry(
section_name: str, package_key: str
) -> None:
"""Reject any stale vulnerable Next.js or sharp package/snapshot entry."""

next_value, eslint_next_value, sharp_value, lock = _frontend_security_inputs()
lock[section_name][package_key] = {}

with pytest.raises(AssertionError):
_assert_lock_contract(lock, next_value, eslint_next_value, sharp_value)
4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.27.0",
"next": "16.2.12",
"next": "16.3.4",
"react": "19.2.8",
"react-dom": "19.2.8",
"react-resizable-panels": "^4.12.2",
Expand All @@ -39,7 +39,7 @@
"@types/react-dom": "^19",
"@vitest/coverage-v8": "4.1.10",
"eslint": "^9",
"eslint-config-next": "16.2.12",
"eslint-config-next": "16.3.4",
"fast-check": "^4.9.0",
"jsdom": "^30.0.1",
"postcss": "8.5.24",
Expand Down
Loading
Loading