COMMITLORE_BIN carried none of the checks its sibling config key already had - #84
Merged
Conversation
…ady had
commitlore hooks install
COMMITLORE_BIN=/tmp/evil.sh git commit -m x
-> evil.sh ran; the commit succeeded
and a .git/config edit made after install could still repoint the recorded
commitlore.bin at an attacker's own script, because naming a file .js costs
nothing:
git config commitlore.bin /tmp/evil.js
git commit -m x
-> evil.js ran; the commit succeeded
This is not remote code execution or privilege escalation. Anyone who can
write .git/config can already write .git/hooks/commit-msg directly -- same
permission, same reach. The bug is inconsistency: commitlore.bin (1fd0d53)
got an extension allowlist and doctor visibility; COMMITLORE_BIN got
neither, so an env var reachable from CI configuration, a sourced profile,
or a compromised toolchain could run anything.
Three changes.
COMMITLORE_BIN now carries the same *.mjs|*.js allowlist commitlore.bin
already had. A value that fails it falls through to the remaining
resolution steps rather than being executed -- the same behaviour the
recorded path has had since 1fd0d53.
The extension check alone does not stop the first reproduction above:
naming a file .js satisfies it for free, and the recorded commitlore.node
still points at a real interpreter. hooks install now also records
commitlore.root -- this installation's own package root, resolved with
realpathSync -- and the stub refuses to run a recorded commitlore.bin whose
physically resolved location (cd "$(dirname ...)" && pwd -P) does not sit
under it. -L rejects the recorded path outright when it is itself a symlink,
closing the gap a directory-only containment check would leave: a symlink
planted inside the root but pointing back out. doctor has warned about a
commitlore.bin outside the package root since 1fd0d53; this is that same
fact enforced, not just reported.
doctor already prints commitlore.bin and an active COMMITLORE_BIN override
(1fd0d53). It now also says when that override will be ignored for failing
the extension check, instead of a reader having to infer it from a runtime
failure reported elsewhere in the same check.
Ruled-out: an install-root check for COMMITLORE_BIN | its only reason to
exist is aiming the hook at a build outside the install root -- a test
harness, a monorepo's local bin. Restricting its location would remove the
one thing it is for; doctor's existing COMMITLORE_BIN visibility is the
right amount of scrutiny for a channel that is supposed to point anywhere.
Ruled-out: resolving commitlore.bin with an external realpath/readlink -f
binary | neither is guaranteed on every machine a hook runs on, the same
reasoning 1fd0d53 gave for not shelling out to shasum. cd ... && pwd -P is
a POSIX-specified builtin and covers the same physical-resolution need
without a new dependency.
Warn: the containment check resolves the recorded path's directory
physically and separately rejects a symlink at the final component, but
does not protect a relative commitlore.bin with no path separator (a bare
filename), which resolves against the hook's cwd and could coincide with
the install root in a repository that installs commitlore against itself.
That residual case sits in the same accepted-risk category as the severity
note above: an attacker who can write that config key already has an
equivalent, direct route.
Verified: reproduced both attacks above against this build after the fix --
neither payload's marker file is created and both commits fail closed
("cannot find the CLI"); a Blast: wide commit is rejected in the same
repository (control: hook is live) and a clean commit is accepted (control:
hook still validates normally); COMMITLORE_BIN pointed at a legitimate
.mjs build still overrides the recorded install and still validates;
a symlink placed inside the install root and pointing at an outside
payload is also refused
Verified: 1236 tests across 34 files, up from the confirmed 1233/34
baseline -- three new hooks.test.ts cases cover both attacks and the
non-regression case (an untampered in-root install still runs);
doctor.test.ts fixtures updated to record commitlore.root, matching what a
real install now does, with one status assertion changed from warn to fail
where the hook genuinely stopped being able to resolve a CLI in a
PATH-less environment
Evidence: src/hooks/commit-msg.ts
Evidence: src/commands/hooks.ts
Evidence: src/commands/doctor.ts
Evidence: src/core/hook-target.ts
Evidence: test/hooks.test.ts
Evidence: test/doctor.test.ts
Follows: r-1e58d3
Blast: system
Undo: easy
Certainty: firm
Record-Id: r-83d43117
Provenance: authored
CommitLore-Version: 2.0.0
X-Claude-Session: https://claude.ai/code/session_014adVVJ3eo6FUiqUVqLGfm9
CommitLore — record lintTrailers: clean — 1 commit in Active constraints for the paths this PR touchesLimits (17)
Ruled out (52)
Warnings (33)
Trailer violations fail this check. Active constraints are informational — they are what the repository already decided, not a verdict on this PR. |
MongLong0214
added a commit
that referenced
this pull request
Jul 27, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Fixes #71 (security).
COMMITLORE_BINletexec "$COMMITLORE_BIN"run any executable the environment pointed at, while the recordedcommitlore.binconfig path had carried an extension allowlist since #64/1fd0d53. An env var is reachable from CI configuration, a sourced profile, or a compromised toolchain — none of which a reviewer reads as executable config.Severity, stated honestly (per the issue): this is not remote code execution and not privilege escalation. Anyone who can write
.git/configcan already write.git/hooks/commit-msgdirectly — same permission, same reach. The bug is inconsistency and auditability, not new capability.Before / after (measured against this branch's build)
evil.shran, commit succeeded.COMMITLORE_BINfails the*.mjs|*.jsallowlist, falls through to the remaining resolution steps.evil.shnever runs; with an invalid trailer present the commit is rejected by real validation.evil.jsran, commit succeeded — naming a file.jssatisfies the existing extension check for free, so that check alone doesn't stop this.hooks installnow also recordscommitlore.root(this installation's own package root). The stub checks the recordedcommitlore.bin's physically-resolved location (cd "$(dirname ...)" && pwd -P, symlinks at the final component rejected via-L) against that root before executing it./tmp/evil.jsis outside the root, so the stub refuses it and falls through; the commit fails closed (cannot find the CLI), andevil.jsnever runs.Controls (both re-verified on this branch): a
Blast: widecommit is rejected in the same repository (hook is live), a clean commit is accepted (hook still validates normally), and a legitimateCOMMITLORE_BINoverride pointed at a real.mjsbuild still works and still validates.Changes
src/hooks/commit-msg.ts—COMMITLORE_BINnow carries the same*.mjs|*.jsallowlist the recorded path already had. Additionally,hooks installrecordscommitlore.root, and the recorded-path branch refuses to execute acommitlore.binwhose resolved directory doesn't sit under it (symlink-at-final-component closed via-L).src/commands/hooks.ts—recordBinPathrecordscommitlore.root(realpathSync(PACKAGE_ROOT)) alongside the existingcommitlore.bin/commitlore.node, best-effort/non-fatal like the rest of that function.src/commands/doctor.ts/src/core/hook-target.ts—doctoralready printedcommitlore.binand an activeCOMMITLORE_BINoverride (since ADR-0012 is accepted but unimplemented: index needs better-sqlite3, so it fails on any fresh install #64/1fd0d53); it now also says when that override will be ignored for failing the extension check, using a sharedhasAllowedBinExtensionpredicate so the shell'scasepattern and the TypeScript check can't disagree.Ruled-out: an install-root containment check for
COMMITLORE_BINitself — its whole reason to exist is aiming the hook at a build outside the install root (a test harness, a monorepo's local bin per the existing docstring). Restricting its location would remove the one thing it's for.Ruled-out: resolving
commitlore.binwith an externalrealpath/readlink -fbinary — neither is guaranteed on every machine a hook runs on (same reasoning #64/1fd0d53 gave for not shelling out toshasum).cd ... && pwd -Pis POSIX-specified and covers the same need with no new dependency.Warn (disclosed residual risk): the containment check doesn't protect a relative, separator-less
commitlore.bin(a bare filename), which resolves against the hook's cwd. This sits in the same accepted-risk category as the severity note above.Test plan
devbefore changes: 1233 passed / 34 files (full suite;test/mcp.test.tsre-run alone too, per known flakiness under load)npm run typecheck,npm run buildclean; committeddist/matches a fresh build ofsrc/COMMITLORE_BINoverride (pointed at a real.mjsbuild) still workstest/hooks.test.tscases;test/doctor.test.tsfixtures updated to recordcommitlore.root, one status assertion changedwarn→failwhere the hook now genuinely can't resolve a CLI in a PATH-less environment)test/dogfood.test.tspasses against this branch's own historynode dist/commitlore.mjs validate --commit HEADpasses on the commit in this PROut of scope (per issue and explicit instructions)
Did not touch MCP path rejection or ANSI stripping, did not change exit-code semantics (bug-issue-65 is in flight separately), not merged to
dev.https://claude.ai/code/session_014adVVJ3eo6FUiqUVqLGfm9