Context
normalizeCommitSha (packages/loopover-miner/lib/replay-snapshot.ts:78-90) accepts any non-empty string with no hex/format check, and the unvalidated value is joined directly into a filesystem path:
function normalizeCommitSha(commitSha: string): string {
if (typeof commitSha !== "string" || !commitSha.trim()) throw new Error("invalid_commit_sha");
return commitSha.trim(); // accepts ANY non-empty string, no hex/format check
}
export const REPLAY_SNAPSHOT_SUBDIR = ".loopover-replay-snapshots";
export function planReplaySnapshotPath(input: { repoPath: string; commitSha: string }): string {
const commitSha = normalizeCommitSha(input.commitSha);
return join(input.repoPath, REPLAY_SNAPSHOT_SUBDIR, commitSha); // join() with unvalidated commitSha
}
This is the same class of bug repo-clone.ts's own comment warns about for owner/repo segments — except here it's commitSha, and unlike replay-task-generation.ts's sibling commit-SHA handling (/^[0-9a-f]{7,40}$/i, used at lines 122/171/192 of that file), replay-snapshot.ts never applies that same format check before using the value in a real path.join().
Verified empirically:
planReplaySnapshotPath({
repoPath: "/home/miner/.config/loopover-miner/repos/acme/widgets",
commitSha: "../../../../../../tmp/evil-worktree",
})
// => "/home/miner/tmp/evil-worktree" (escapes repoPath entirely)
addDetachedWorktree would then run git worktree add --detach <that escaped path> <commitSha> — a crafted commitSha controls where on disk a git worktree gets created, outside the intended .loopover-replay-snapshots sandbox subdirectory.
Current reach: exportReplaySnapshot/planReplaySnapshotPath has zero production callers today (confirmed via repo-wide grep — only test/unit/miner-replay-snapshot.test.ts calls it; replay-task-bridge.ts's own header comment confirms "nothing calls exportReplaySnapshot yet either"). This is a latent, well-scoped correctness/hardening gap in code that's about to gain a real caller as the replay/calibration feature is wired up — not a currently-live vulnerability, since nothing reachable from a live CLI path calls it yet. The existing test suite never passes a malformed/traversal-shaped commitSha — every case uses simple values like "abc123".
Requirements
Reuse replay-task-generation.ts's existing /^[0-9a-f]{7,40}$/i commit-SHA validator (or an equivalent format check) in normalizeCommitSha, so a non-hex or path-traversal-shaped commitSha is rejected before it ever reaches path.join().
Deliverables
Test Coverage Requirements
This file is under src/ via the packages/loopover-miner workspace - confirm current coverage.include scoping before assuming the top-level 99% patch gate applies exactly as src/** does; the new guard branch and its test must both be covered.
Expected Outcome
A malformed or path-traversal-shaped commitSha passed to the replay-snapshot path planner is rejected outright, instead of silently producing a path outside the intended .loopover-replay-snapshots sandbox directory - closing this gap before the in-progress replay/calibration feature gains a live caller.
Links & Resources
packages/loopover-miner/lib/replay-snapshot.ts:78-90, packages/loopover-miner/lib/replay-task-generation.ts:122,171,192 (the existing commit-SHA validator to reuse), packages/loopover-miner/lib/repo-clone.ts (the analogous owner/repo path-safety concern this mirrors)
Context
normalizeCommitSha(packages/loopover-miner/lib/replay-snapshot.ts:78-90) accepts any non-empty string with no hex/format check, and the unvalidated value is joined directly into a filesystem path:This is the same class of bug
repo-clone.ts's own comment warns about for owner/repo segments — except here it'scommitSha, and unlikereplay-task-generation.ts's sibling commit-SHA handling (/^[0-9a-f]{7,40}$/i, used at lines 122/171/192 of that file),replay-snapshot.tsnever applies that same format check before using the value in a realpath.join().Verified empirically:
addDetachedWorktreewould then rungit worktree add --detach <that escaped path> <commitSha>— a craftedcommitShacontrols where on disk a git worktree gets created, outside the intended.loopover-replay-snapshotssandbox subdirectory.Current reach:
exportReplaySnapshot/planReplaySnapshotPathhas zero production callers today (confirmed via repo-wide grep — onlytest/unit/miner-replay-snapshot.test.tscalls it;replay-task-bridge.ts's own header comment confirms "nothing calls exportReplaySnapshot yet either"). This is a latent, well-scoped correctness/hardening gap in code that's about to gain a real caller as the replay/calibration feature is wired up — not a currently-live vulnerability, since nothing reachable from a live CLI path calls it yet. The existing test suite never passes a malformed/traversal-shapedcommitSha— every case uses simple values like"abc123".Requirements
Reuse
replay-task-generation.ts's existing/^[0-9a-f]{7,40}$/icommit-SHA validator (or an equivalent format check) innormalizeCommitSha, so a non-hex or path-traversal-shapedcommitShais rejected before it ever reachespath.join().Deliverables
normalizeCommitShainpackages/loopover-miner/lib/replay-snapshot.tsrejects any value that isn't a valid commit-SHA-shaped hex string, matchingreplay-task-generation.ts's existing validator.commitSha(e.g. containing../), assertingplanReplaySnapshotPath/normalizeCommitSharejects it instead of producing an escaped path.Test Coverage Requirements
This file is under
src/via thepackages/loopover-minerworkspace - confirm currentcoverage.includescoping before assuming the top-level 99% patch gate applies exactly assrc/**does; the new guard branch and its test must both be covered.Expected Outcome
A malformed or path-traversal-shaped commitSha passed to the replay-snapshot path planner is rejected outright, instead of silently producing a path outside the intended
.loopover-replay-snapshotssandbox directory - closing this gap before the in-progress replay/calibration feature gains a live caller.Links & Resources
packages/loopover-miner/lib/replay-snapshot.ts:78-90,packages/loopover-miner/lib/replay-task-generation.ts:122,171,192(the existing commit-SHA validator to reuse),packages/loopover-miner/lib/repo-clone.ts(the analogous owner/repo path-safety concern this mirrors)