Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions packages/loopover-miner/lib/contribution-profile-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
openLocalStoreAdapter,
resolveLocalStoreDbPath,
} from "./local-store.js";
import { isValidRepoSegment } from "./repo-clone.js";
import { applySchemaMigrations } from "./schema-version.js";
import {
CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC,
Expand Down Expand Up @@ -58,6 +59,8 @@ function normalizeRepoFullName(repoFullName: unknown): string {
const [owner, repo, extra] = repoFullName.trim().split("/");
if (!owner || !repo || extra !== undefined)
throw new Error("invalid_repo_full_name");
if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo))
throw new Error("invalid_repo_full_name");
return `${owner}/${repo}`;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/loopover-miner/lib/prediction-ledger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DatabaseSync } from "node:sqlite";
import { normalizeLocalStoreDbPath, openLocalStoreAdapter, resolveLocalStoreDbPath } from "./local-store.js";
import { isValidRepoSegment } from "./repo-clone.js";
import { applySchemaMigrations } from "./schema-version.js";
import {
PREDICTION_LEDGER_PURGE_SPEC,
Expand Down Expand Up @@ -87,6 +88,7 @@ function normalizeRepoFullName(repoFullName: string): string {
if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name");
const [owner, repo, extra] = repoFullName.trim().split("/");
if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) throw new Error("invalid_repo_full_name");
return `${owner}/${repo}`;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/loopover-miner/lib/replay-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { join } from "node:path";
import { removeWorktree } from "@loopover/engine";
import type { WorktreeExecFn, WorktreeRemoveResult } from "@loopover/engine";
import { openLocalStoreAdapter, resolveLocalStoreDbPath, normalizeLocalStoreDbPath } from "./local-store.js";
import { isValidRepoSegment } from "./repo-clone.js";

// Freeze/snapshot mechanism for historical replay targets (#3010). Given a repo and a commit SHA T, exports:
// (a) the full working tree checked out AT T via a DETACHED git worktree -- the same isolation primitive
Expand Down Expand Up @@ -72,6 +73,7 @@ function normalizeRepoFullName(repoFullName: string): string {
if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name");
const [owner, repo, extra] = repoFullName.trim().split("/");
if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) throw new Error("invalid_repo_full_name");
return `${owner}/${repo}`;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/loopover-miner/lib/run-state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DatabaseSync } from "node:sqlite";
import { DEFAULT_FORGE_CONFIG } from "./forge-config.js";
import { normalizeLocalStoreDbPath, openLocalStoreAdapter, resolveLocalStoreDbPath } from "./local-store.js";
import { isValidRepoSegment } from "./repo-clone.js";
import { applySchemaMigrations } from "./schema-version.js";
import { RUN_STATE_PURGE_SPEC, purgeStoreByRepo } from "./store-maintenance.js";

Expand Down Expand Up @@ -57,6 +58,7 @@ function normalizeRepoFullName(repoFullName: string): string {
const trimmed = repoFullName.trim();
const [owner, repo, extra] = trimmed.split("/");
if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) throw new Error("invalid_repo_full_name");
return `${owner}/${repo}`;
}

Expand Down
4 changes: 4 additions & 0 deletions test/unit/miner-contribution-profile-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ describe("contribution-profile cache store (#6797)", () => {
expect(() => store.put({ repoFullName: 42 } as never)).toThrow(
"invalid_repo_full_name",
);
// #7795: path-traversal / invalid-character segments must fail closed like claim-ledger (#5831).
expect(() => store.get("../etc")).toThrow("invalid_repo_full_name");
expect(() => store.get("o/..")).toThrow("invalid_repo_full_name");
expect(() => store.put({ repoFullName: "o baz/a" } as never)).toThrow("invalid_repo_full_name");
});

it("exposes module-level get/put helpers backed by the default DB path", () => {
Expand Down
7 changes: 7 additions & 0 deletions test/unit/miner-prediction-ledger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ describe("miner prediction ledger (#4263)", () => {
expect(() => ledger.appendPrediction({ ...VALID, readinessScore: Number.NaN })).toThrow(/invalid_readiness_score/);
});

it("REGRESSION (#7795): rejects path-traversal or invalid-character repo segments", () => {
const ledger = tempLedger();
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "../etc" })).toThrow(/invalid_repo_full_name/);
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "o/.." })).toThrow(/invalid_repo_full_name/);
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "o baz/a" })).toThrow(/invalid_repo_full_name/);
});

it("scopes readPredictions by repo, preserving insertion order", () => {
const ledger = tempLedger();
ledger.appendPrediction({ ...VALID, repoFullName: "owner/repo-a", targetId: 1 });
Expand Down
4 changes: 4 additions & 0 deletions test/unit/miner-replay-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,10 @@ describe("exportReplaySnapshot (#3010)", () => {

await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "noslash", commitSha: "a" }, deps)).rejects.toThrow("invalid_repo_full_name");
await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "a/b/c", commitSha: "a" }, deps)).rejects.toThrow("invalid_repo_full_name");
// #7795: path-traversal / invalid-character segments must fail closed like claim-ledger (#5831).
await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "../etc", commitSha: "a" }, deps)).rejects.toThrow("invalid_repo_full_name");
await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "o/..", commitSha: "a" }, deps)).rejects.toThrow("invalid_repo_full_name");
await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "o baz/a", commitSha: "a" }, deps)).rejects.toThrow("invalid_repo_full_name");
});

it("assertExecResult falls back to a generic exit-code message when stderr is entirely absent", async () => {
Expand Down
4 changes: 4 additions & 0 deletions test/unit/miner-run-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ describe("loopover-miner run-state store (#2289)", () => {
expect(() => store.setRunState("owner/repo/extra", "idle")).toThrow("invalid_repo_full_name");
expect(() => store.setRunState("owner/repo", "blocked" as never)).toThrow("invalid_run_state");
expect(store.getRunState("owner/repo")).toBeNull();
// #7795: path-traversal / invalid-character segments must fail closed like claim-ledger (#5831).
expect(() => store.getRunState("../etc")).toThrow("invalid_repo_full_name");
expect(() => store.setRunState("o/..", "idle")).toThrow("invalid_repo_full_name");
expect(() => store.setRunState("o baz/a", "idle")).toThrow("invalid_repo_full_name");
} finally {
store.close();
}
Expand Down
Loading