-
Notifications
You must be signed in to change notification settings - Fork 1
fix(deps): patch frontend audit security floors #1623
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
seonghobae
wants to merge
16
commits into
develop
Choose a base branch
from
autoresearch/frontend-sec-bump
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 13 commits
Commits
Show all changes
16 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 d76f7b4
test(security): lock frontend patched dependency floors
seonghobae a5abe04
test(deps): reproduce weak frontend lock validation
seonghobae e8a54fc
fix(deps): validate frontend lock security floors structurally
seonghobae b97f42f
fix(deps): raise frontend audit security floors
seonghobae 09cb87a
test(deps): bind js-yaml owner succession structurally
seonghobae a6715c9
test(deps): reproduce missing Vitest lock-entry gap
seonghobae 21897d8
test(deps): require Vitest lock resolutions
seonghobae 15fecaa
test(security): reproduce Vitest importer drift
seonghobae 17a7618
test(security): bind Vitest importer contract
seonghobae 5487141
fix(deps): bind Vitest importer package records
seonghobae 4e66036
docs(gap): record Vitest lockfile repair evidence
seonghobae d8327d4
fix(governance): keep dependency PR off canonical gap ledger
seonghobae 8175f7f
test(deps): require patched Nano ID resolution
seonghobae 9d6d1e0
fix(deps): pin patched Nano ID in pnpm owner
seonghobae ecbeb7a
test(deps): bind PostCSS floor to reviewed manifest
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
353 changes: 353 additions & 0 deletions
353
backend/tests/test_frontend_framework_security_floor.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,353 @@ | ||
| """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) | ||
| JS_YAML_SECURITY_FLOOR = (4, 3, 2) | ||
| VITEST_SECURITY_FLOOR = (4, 1, 11) | ||
|
|
||
|
|
||
| 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) | ||
|
|
||
|
|
||
| def test_js_yaml_security_floor_covers_every_lock_resolution() -> None: | ||
| """Keep every js-yaml resolution above the reviewed denial-of-service floor.""" | ||
|
|
||
| lock = yaml.safe_load( | ||
| (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") | ||
| ) | ||
| for section_name in ("packages", "snapshots"): | ||
| js_yaml_keys = [ | ||
| key for key in lock[section_name] if key.startswith("js-yaml@") | ||
| ] | ||
| for package_key in js_yaml_keys: | ||
| assert ( | ||
| _package_key_version(package_key, "js-yaml") | ||
| >= JS_YAML_SECURITY_FLOOR | ||
| ), f"{section_name} contains js-yaml below the reviewed security floor" | ||
|
|
||
|
|
||
| def test_vitest_security_floor_covers_manifest_and_lock() -> None: | ||
| """Keep Vitest and its coverage package above the reviewed traversal floor.""" | ||
|
|
||
| package = json.loads((FRONTEND_ROOT / "package.json").read_text(encoding="utf-8")) | ||
| lock = yaml.safe_load( | ||
| (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") | ||
| ) | ||
| importer = lock["importers"]["."]["devDependencies"] | ||
| for package_name in ("vitest", "@vitest/coverage-v8"): | ||
| declared_value = package["devDependencies"][package_name] | ||
| assert _exact_version(declared_value) >= VITEST_SECURITY_FLOOR | ||
| importer_entry = importer[package_name] | ||
| assert importer_entry["specifier"] == declared_value, ( | ||
| f"root importer must preserve the package.json {package_name} specifier" | ||
| ) | ||
| assert _resolved_version(str(importer_entry["version"])) == _exact_version( | ||
| declared_value | ||
| ), f"root importer must resolve the reviewed {package_name} release" | ||
| resolved_version = str(importer_entry["version"]) | ||
| base_version = resolved_version.split("(", 1)[0] | ||
| assert f"{package_name}@{base_version}" in lock["packages"], ( | ||
| f"root importer {package_name} resolution must reference an existing package record" | ||
| ) | ||
| assert f"{package_name}@{importer_entry['version']}" in lock["snapshots"], ( | ||
| f"root importer {package_name} resolution must reference an existing snapshot" | ||
| ) | ||
| for section_name in ("packages", "snapshots"): | ||
| package_keys = [ | ||
| package_key | ||
| for package_key in lock[section_name] | ||
| if package_key.startswith(f"{package_name}@") | ||
| ] | ||
| assert package_keys, ( | ||
| f"{section_name} must contain a {package_name} resolution" | ||
| ) | ||
| for package_key in package_keys: | ||
| assert ( | ||
| _package_key_version(package_key, package_name) | ||
| >= VITEST_SECURITY_FLOOR | ||
| ), f"{section_name} contains {package_name} below the reviewed floor" | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) | ||
| @pytest.mark.parametrize("section_name", ["packages", "snapshots"]) | ||
| def test_vitest_security_floor_rejects_missing_lock_resolution( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| package_name: str, | ||
| section_name: str, | ||
| ) -> None: | ||
| """Reject a regenerated lock section that drops an expected Vitest resolution.""" | ||
|
|
||
| package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") | ||
| lock = yaml.safe_load( | ||
| (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") | ||
| ) | ||
| lock[section_name] = { | ||
| key: value | ||
| for key, value in lock[section_name].items() | ||
| if not key.startswith(f"{package_name}@") | ||
| } | ||
| lock_text = yaml.safe_dump(lock) | ||
| original_read_text = Path.read_text | ||
|
|
||
| def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: | ||
| if path == FRONTEND_ROOT / "package.json": | ||
| return package_text | ||
| if path == FRONTEND_ROOT / "pnpm-lock.yaml": | ||
| return lock_text | ||
| return original_read_text(path, *args, **kwargs) | ||
|
|
||
| monkeypatch.setattr(Path, "read_text", _read_text) | ||
| with pytest.raises(AssertionError): | ||
| test_vitest_security_floor_covers_manifest_and_lock() | ||
|
|
||
|
|
||
| @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) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) | ||
| @pytest.mark.parametrize("field", ["specifier", "version"]) | ||
| def test_vitest_security_floor_rejects_root_importer_drift( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| package_name: str, | ||
| field: str, | ||
| ) -> None: | ||
| """Reject a root Vitest importer that no longer matches the reviewed manifest.""" | ||
|
|
||
| package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") | ||
| lock = yaml.safe_load( | ||
| (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") | ||
| ) | ||
| lock["importers"]["."]["devDependencies"][package_name][field] = "4.1.12" | ||
| lock_text = yaml.safe_dump(lock) | ||
| original_read_text = Path.read_text | ||
|
|
||
| def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: | ||
| if path == FRONTEND_ROOT / "package.json": | ||
| return package_text | ||
| if path == FRONTEND_ROOT / "pnpm-lock.yaml": | ||
| return lock_text | ||
| return original_read_text(path, *args, **kwargs) | ||
|
|
||
| monkeypatch.setattr(Path, "read_text", _read_text) | ||
| with pytest.raises(AssertionError): | ||
| test_vitest_security_floor_covers_manifest_and_lock() | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) | ||
| def test_vitest_security_floor_rejects_missing_root_snapshot( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| package_name: str, | ||
| ) -> None: | ||
| """Reject a root Vitest resolution whose exact peer-qualified snapshot vanished.""" | ||
|
|
||
| package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") | ||
| lock = yaml.safe_load( | ||
| (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") | ||
| ) | ||
| resolution = str( | ||
| lock["importers"]["."]["devDependencies"][package_name]["version"] | ||
| ) | ||
| snapshot_key = f"{package_name}@{resolution}" | ||
| snapshot = lock["snapshots"].pop(snapshot_key) | ||
| lock["snapshots"][f"{package_name}@4.1.12"] = snapshot | ||
| lock_text = yaml.safe_dump(lock) | ||
| original_read_text = Path.read_text | ||
|
|
||
| def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: | ||
| if path == FRONTEND_ROOT / "package.json": | ||
| return package_text | ||
| if path == FRONTEND_ROOT / "pnpm-lock.yaml": | ||
| return lock_text | ||
| return original_read_text(path, *args, **kwargs) | ||
|
|
||
| monkeypatch.setattr(Path, "read_text", _read_text) | ||
| with pytest.raises(AssertionError): | ||
| test_vitest_security_floor_covers_manifest_and_lock() | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("package_name", ["vitest", "@vitest/coverage-v8"]) | ||
| def test_vitest_security_floor_rejects_missing_root_package( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| package_name: str, | ||
| ) -> None: | ||
| """Reject a root Vitest resolution whose base package record vanished.""" | ||
|
|
||
| package_text = (FRONTEND_ROOT / "package.json").read_text(encoding="utf-8") | ||
| lock = yaml.safe_load( | ||
| (FRONTEND_ROOT / "pnpm-lock.yaml").read_text(encoding="utf-8") | ||
| ) | ||
| resolution = str( | ||
| lock["importers"]["."]["devDependencies"][package_name]["version"] | ||
| ) | ||
| package_key = f"{package_name}@{resolution.split('(', 1)[0]}" | ||
| package_record = lock["packages"].pop(package_key) | ||
| lock["packages"][f"{package_name}@4.1.12"] = package_record | ||
| lock_text = yaml.safe_dump(lock) | ||
| original_read_text = Path.read_text | ||
|
|
||
| def _read_text(path: Path, *args: Any, **kwargs: Any) -> str: | ||
| if path == FRONTEND_ROOT / "package.json": | ||
| return package_text | ||
| if path == FRONTEND_ROOT / "pnpm-lock.yaml": | ||
| return lock_text | ||
| return original_read_text(path, *args, **kwargs) | ||
|
|
||
| monkeypatch.setattr(Path, "read_text", _read_text) | ||
| with pytest.raises(AssertionError): | ||
| test_vitest_security_floor_covers_manifest_and_lock() | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.