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
4 changes: 4 additions & 0 deletions packages/loopover-miner/lib/contribution-profile-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
resolveLocalStoreDbPath,
} from "./local-store.js";
import { applySchemaMigrations } from "./schema-version.js";
import { isValidRepoSegment } from "./repo-clone.js";
import {
CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC,
purgeStoreByRepo,
Expand Down Expand Up @@ -58,6 +59,9 @@ function normalizeRepoFullName(repoFullName: unknown): string {
const [owner, repo, extra] = repoFullName.trim().split("/");
if (!owner || !repo || extra !== undefined)
throw new Error("invalid_repo_full_name");
// #7795: reject `.`/`..`/control-char segments (via repo-clone.js's shared guard) before this value backs a
// SQLite key or is echoed through a CLI -- the same path-safety check #5831/#7525 rolled out to every sibling.
if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) throw new Error("invalid_repo_full_name");
return `${owner}/${repo}`;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/loopover-miner/lib/prediction-ledger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DatabaseSync } from "node:sqlite";
import { normalizeLocalStoreDbPath, openLocalStoreAdapter, resolveLocalStoreDbPath } from "./local-store.js";
import { applySchemaMigrations } from "./schema-version.js";
import { isValidRepoSegment } from "./repo-clone.js";
import {
PREDICTION_LEDGER_PURGE_SPEC,
PREDICTION_LEDGER_RETENTION_SPEC,
Expand Down Expand Up @@ -87,6 +88,9 @@ 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");
// #7795: reject `.`/`..`/control-char segments (via repo-clone.js's shared guard) before this value backs a
// SQLite key or is echoed through a CLI -- the same path-safety check #5831/#7525 rolled out to every sibling.
if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) throw new Error("invalid_repo_full_name");
return `${owner}/${repo}`;
}

Expand Down
4 changes: 4 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,9 @@ 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");
// #7795: reject `.`/`..`/control-char segments (via repo-clone.js's shared guard) before this value backs a
// SQLite key or is echoed through a CLI -- the same path-safety check #5831/#7525 rolled out to every sibling.
if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) throw new Error("invalid_repo_full_name");
return `${owner}/${repo}`;
}

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

export type RunState = "idle" | "discovering" | "planning" | "preparing";
Expand Down Expand Up @@ -57,6 +58,9 @@ 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");
// #7795: reject `.`/`..`/control-char segments (via repo-clone.js's shared guard) before this value backs a
// SQLite key or is echoed through a CLI -- the same path-safety check #5831/#7525 rolled out to every sibling.
if (!isValidRepoSegment(owner) || !isValidRepoSegment(repo)) throw new Error("invalid_repo_full_name");
return `${owner}/${repo}`;
}

Expand Down
3 changes: 3 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,9 @@ describe("contribution-profile cache store (#6797)", () => {
expect(() => store.put({ repoFullName: 42 } as never)).toThrow(
"invalid_repo_full_name",
);
// #7795: `.`/`..`/control-char segments must be rejected too (both owner and repo), not just missing/extra slashes.
expect(() => store.get("../etc")).toThrow("invalid_repo_full_name");
expect(() => store.get("owner/..")).toThrow("invalid_repo_full_name");
});

it("exposes module-level get/put helpers backed by the default DB path", () => {
Expand Down
3 changes: 3 additions & 0 deletions test/unit/miner-prediction-ledger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ describe("miner prediction ledger (#4263)", () => {
it("rejects invalid inputs field by field", () => {
const ledger = tempLedger();
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "no-slash" })).toThrow(/invalid_repo_full_name/);
// #7795: `.`/`..`/control-char segments must be rejected too (both owner and repo), not just missing/extra slashes.
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "../etc" })).toThrow(/invalid_repo_full_name/);
expect(() => ledger.appendPrediction({ ...VALID, repoFullName: "owner/.." })).toThrow(/invalid_repo_full_name/);
expect(() => ledger.appendPrediction({ ...VALID, targetId: 0 })).toThrow(/invalid_target_id/);
expect(() => ledger.appendPrediction({ ...VALID, conclusion: "" })).toThrow(/invalid_conclusion/);
expect(() => ledger.appendPrediction({ ...VALID, engineVersion: "" })).toThrow(/invalid_engine_version/);
Expand Down
3 changes: 3 additions & 0 deletions test/unit/miner-replay-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,9 @@ 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: `.`/`..`/control-char segments must be rejected too (both owner and repo), not just missing/extra slashes.
await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "../etc", commitSha: "a" }, deps)).rejects.toThrow("invalid_repo_full_name");
await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "owner/..", 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
3 changes: 3 additions & 0 deletions test/unit/miner-run-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ describe("loopover-miner run-state store (#2289)", () => {
try {
expect(() => store.getRunState("not-a-full-name")).toThrow("invalid_repo_full_name");
expect(() => store.setRunState("owner/repo/extra", "idle")).toThrow("invalid_repo_full_name");
// #7795: `.`/`..`/control-char segments must be rejected too (both owner and repo), not just missing/extra slashes.
expect(() => store.getRunState("../etc")).toThrow("invalid_repo_full_name");
expect(() => store.setRunState("owner/..", "idle")).toThrow("invalid_repo_full_name");
expect(() => store.setRunState("owner/repo", "blocked" as never)).toThrow("invalid_run_state");
expect(store.getRunState("owner/repo")).toBeNull();
} finally {
Expand Down