fix(core): validate config.env, enforce version pinning, fix CI trust anchor and resolve_catalog guards (#68) #70
Reference in New Issue
Block a user
Delete Branch "fix/68-core-validation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes findings 2, 3, 4, 6, 7 from #68 (the second adversarial review). Scope limited to
bcc_core.py,tests/test_core.py,.github/workflows/ci.ymlper the issue — findings 1 and 5 (Console signing flow, key separation) are out of scope for this PR and are being handled separately.Finding 2 —
config.envwas unvalidated_validate_catalog_configtype-checkedenvonly. Added:CATALOG_DENIED_ENV_KEYS(module-level frozenset, importable by the Console):NODE_OPTIONS,PYTHONSTARTUP,PYTHONPATH,PYTHONHOME,LD_PRELOAD,LD_LIBRARY_PATH,DYLD_INSERT_LIBRARIES,DYLD_LIBRARY_PATH,BROWSER,PATH,NODE_REPL_EXTERNAL_MODULE. Matched case-insensitively.argsgets, applied to env keys and values._is_secret_valuecheck applied to env values.config.envvalues must be""or a single<PLACEHOLDER>token — blocking otherwise.Reproduced and confirmed blocked:
{"command": "npx", "env": {"NODE_OPTIONS": "--require /tmp/x.js"}}now failsvalidate_catalog.Finding 3 — version pinning not enforced at runtime
Moved enforcement from
catalog_review.py(maintainer-only, never runs per finding 1) into_validate_catalog_config, blocking:@version(scoped@scope/pkg@1.2.3handled correctly — right-to-left parse so the scope's leading@isn't mistaken for the version separator).@versionor==version.latest.Only the first plausible package-spec token is checked (flags,
<PLACEHOLDER>s, and docker'srun/-i/--rm/-e KEYare correctly skipped). All 19 entries in the realdata/catalog.jsonstill validate clean (test_shipped_catalog_json_passes_validation— unedited, still green).Finding 4 — CI gate trusted the pubkey from the PR it reviewed
ci.yml'scatalog-signaturejob now hardcodesEXPECTED_CATALOG_PUBKEY_B64and assertsbcc_core.CATALOG_PUBKEYS(as loaded from the PR branch) matches it, before verifying the signature. Comment on that line calls out that it's a trust anchor and must be reviewed on its own, never bundled with a catalog content change.Verified the actual attack in a sandbox: cloned the repo, had an "attacker" generate their own Ed25519 keypair, re-signed
data/catalog.jsonwith it, and swappedbcc_core.CATALOG_PUBKEYSto the attacker's key (the exact two-file diff from the finding) — the extracted CI check now exits 1 with a clear message instead of going green.Note for the maintainer: per the issue, the current key (
082NOwVB7uURkvfyS3+knJ+40Fk6C9unsF47+2uPKo4=) is about to be rotated.EXPECTED_CATALOG_PUBKEY_B64inci.ymlis a single obvious line specifically so that update is trivial — please review it as its own commit when you rotate.Finding 6 —
resolve_catalogguards skipped the first candidateBoth guards sat behind
if best_version >= 0, so the first verified candidate (bundled, cached, or remote — whichever landed first) was accepted unconditionally, and the anti-freeze ceiling drifted upward with each accepted candidate instead of staying fixed (a legitimate moderate bump could "carry" a much larger illegitimate jump right behind it).floor: int = 0parameter so a future caller can pass a persisted accepted-version floor; no storage wired up in this PR, as scoped.Finding 7 — catalog
idunconstrainedConstrained to
^[a-z0-9][a-z0-9._-]{0,63}$.Tests
_minimal_catalog/test_load_catalog_valid_round_trip— the fixture used an unpinned package and asserted it validated clean (enshrined finding 3). Now pinned; added a separate test asserting the unpinned form is rejected.test_catalog_entry_to_paste_json_includes_env_when_present— it assertedenvpasses through verbatim without checkingvalidate_catalogfirst rejects the dangerous case (enshrined finding 2).test_resolve_catalog_rejects_absurd_version_jumpso the freeze attempt is evaluated first, which is what actually exercises the vulnerable path; addedtest_resolve_catalog_caps_first_and_only_candidate,test_resolve_catalog_anchors_cap_to_bundled_not_a_chained_best,test_resolve_catalog_prefers_bundled_on_version_tie,test_resolve_catalog_floor_rejects_below_persisted_version.:latest/untagged docker, badidforms).Status
ruff check .— cleanruff format --check .— cleanpytest— 347 passed, 1 skipped (pre-existing skip, unrelated)data/catalog.jsonuntouched;test_shipped_catalog_json_passes_validationstill green with the new rulesCloses findings 2, 3, 4, 6, 7 of #68.