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
2 changes: 2 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,7 @@ 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
14 changes: 14 additions & 0 deletions test/unit/miner-contribution-profile-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,20 @@ describe("contribution-profile cache store (#6797)", () => {
);
});

// #7795: an unsafe path-traversal/invalid-character segment must be rejected here too, matching
// repo-clone.js's own validation, instead of being silently accepted and persisted as a cache key --
// for both the owner and repo segment independently.
it("rejects a repoFullName with a path-traversal or invalid-character segment", () => {
const store = tempStore();
// Reads (get) and writes (put) both funnel through normalizeRepoFullName.
expect(() => store.get("../etc")).toThrow("invalid_repo_full_name"); // owner ".." invalid
expect(() => store.get("o/..")).toThrow("invalid_repo_full_name"); // repo ".." invalid
expect(() => store.get("o baz/a")).toThrow("invalid_repo_full_name");
expect(() => store.get("o/a baz")).toThrow("invalid_repo_full_name");
expect(() => store.put(profile("../etc"), AT_MS)).toThrow("invalid_repo_full_name");
expect(() => store.put(profile("o/.."), AT_MS)).toThrow("invalid_repo_full_name");
});

it("exposes module-level get/put helpers backed by the default DB path", () => {
vi.stubEnv(
"LOOPOVER_MINER_CONTRIBUTION_PROFILE_CACHE_DB",
Expand Down
14 changes: 14 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,20 @@ describe("miner prediction ledger (#4263)", () => {
expect(() => ledger.appendPrediction({ ...VALID, readinessScore: Number.NaN })).toThrow(/invalid_readiness_score/);
});

// #7795: an unsafe path-traversal/invalid-character segment must be rejected here too, matching
// repo-clone.js's own validation, instead of being silently accepted and persisted as a ledger key --
// for both the owner and repo segment independently.
it("rejects a repoFullName with a path-traversal or invalid-character segment", () => {
const ledger = tempLedger();
// Both appendPrediction (write) and readPredictions (read) funnel through normalizeRepoFullName.
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "../etc" })).toThrow("invalid_repo_full_name"); // owner ".." invalid
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "o/.." })).toThrow("invalid_repo_full_name"); // repo ".." invalid
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "o baz/a" })).toThrow("invalid_repo_full_name");
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "o/a baz" })).toThrow("invalid_repo_full_name");
expect(() => ledger.readPredictions({ repoFullName: "../etc" })).toThrow("invalid_repo_full_name");
expect(() => ledger.readPredictions({ repoFullName: "o/.." })).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
11 changes: 11 additions & 0 deletions test/unit/miner-replay-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,4 +456,15 @@ describe("openReplaySnapshotStore (#3010) — round-trip persistence", () => {
const store = tempStore();
expect(store.getSnapshot("acme/widgets", "nope")).toBeNull();
});

// #7795: an unsafe path-traversal/invalid-character segment must be rejected here too, matching
// repo-clone.js's own validation, instead of being silently accepted and used as a snapshot key --
// for both the owner and repo segment independently.
it("rejects a repoFullName with a path-traversal or invalid-character segment", () => {
const store = tempStore();
expect(() => store.getSnapshot("../etc", "abc123")).toThrow("invalid_repo_full_name"); // owner ".." invalid
expect(() => store.getSnapshot("o/..", "abc123")).toThrow("invalid_repo_full_name"); // repo ".." invalid
expect(() => store.getSnapshot("o baz/a", "abc123")).toThrow("invalid_repo_full_name");
expect(() => store.getSnapshot("o/a baz", "abc123")).toThrow("invalid_repo_full_name");
});
});
18 changes: 18 additions & 0 deletions test/unit/miner-run-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,24 @@ describe("loopover-miner run-state store (#2289)", () => {
}
});

// #7795: an unsafe path-traversal/invalid-character segment must be rejected here too, matching
// repo-clone.js's own validation, instead of being silently accepted and persisted as a state key --
// for both the owner and repo segment independently.
it("rejects a repoFullName with a path-traversal or invalid-character segment", () => {
const store = initRunStateStore(join(tempRoot(), "run-state.sqlite3"));
try {
// Both getRunState (read) and setRunState (write) funnel through normalizeRepoFullName.
expect(() => store.getRunState("../etc")).toThrow("invalid_repo_full_name"); // owner ".." invalid
expect(() => store.getRunState("o/..")).toThrow("invalid_repo_full_name"); // repo ".." invalid
expect(() => store.setRunState("o baz/a", "idle")).toThrow("invalid_repo_full_name");
expect(() => store.setRunState("o/a baz", "idle")).toThrow("invalid_repo_full_name");
expect(() => store.setRunState("../etc", "idle")).toThrow("invalid_repo_full_name");
expect(() => store.setRunState("o/..", "idle")).toThrow("invalid_repo_full_name");
} finally {
store.close();
}
});

it("fails closed to null when a legacy table contains an unknown state", () => {
const dbPath = join(tempRoot(), "legacy.sqlite3");
const legacy = new DatabaseSync(dbPath);
Expand Down