Skip to content

Commit a1d61eb

Browse files
feat(daemon): add POST /git/raw endpoint with allowlist validation (#1163)
* feat(daemon): add POST /git/raw endpoint with allowlist validation Exposes git.raw() via HTTP with safety guardrails: only a curated set of read/non-destructive subcommands is allowed (checkout, branch, stash, tag, log, show, diff, merge, cherry-pick, etc.), and dangerous flags (--force, --hard, --global, --system, etc.) are blocked regardless of subcommand. Made-with: Cursor * Update daemon/git.ts Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
1 parent 5983a2f commit a1d61eb

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed

daemon/git.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,79 @@ export interface CheckoutBranchAPI {
324324
};
325325
}
326326

327+
// ─── /git/raw ─────────────────────────────────────────────────────────────────
328+
329+
export interface GitRawAPI {
330+
body: {
331+
args: string[];
332+
};
333+
response: {
334+
result: string;
335+
};
336+
}
337+
338+
/**
339+
* Git subcommands that are safe to run via /git/raw.
340+
* Intentionally excludes push, pull, clone, remote, config, gc, and other
341+
* operations that can affect the remote, global config, or be irreversible.
342+
*/
343+
const ALLOWED_SUBCOMMANDS = new Set([
344+
"checkout",
345+
"branch",
346+
"stash",
347+
"tag",
348+
"log",
349+
"show",
350+
"diff",
351+
"merge",
352+
"cherry-pick",
353+
"format-patch",
354+
"describe",
355+
"shortlog",
356+
"rev-parse",
357+
"rev-list",
358+
"ls-files",
359+
"ls-tree",
360+
"cat-file",
361+
]);
362+
363+
/**
364+
* Flags that are blocked regardless of subcommand, as they can cause
365+
* irreversible damage or break system-level git configuration.
366+
*/
367+
const BLOCKED_FLAGS = new Set([
368+
"--force",
369+
"-f",
370+
"--hard",
371+
"--global",
372+
"--system",
373+
"--exec",
374+
]);
375+
376+
const validateRawArgs = (args: string[]): string | null => {
377+
if (!args.length) return "No git arguments provided";
378+
379+
const subcommand = args[0];
380+
if (!ALLOWED_SUBCOMMANDS.has(subcommand)) {
381+
return `Subcommand "${subcommand}" is not allowed. Allowed: ${[...ALLOWED_SUBCOMMANDS].join(", ")}`;
382+
}
383+
384+
const blockedFlag = args.find((arg) => {
385+
if (arg.startsWith("--")) {
386+
return BLOCKED_FLAGS.has(arg.split("=")[0]);
387+
}
388+
if (arg.startsWith("-") && arg.length > 1) {
389+
return [...arg.slice(1)].some((ch) => BLOCKED_FLAGS.has(`-${ch}`));
390+
}
391+
return false;
392+
});
393+
if (blockedFlag) {
394+
return `Flag "${blockedFlag}" is blocked`;
395+
}
396+
397+
return null;
398+
};
399+
327400
const abortRebase = async () => {
328401
await git.rebase({ "--abort": null });
329402
throw new Error(
@@ -760,6 +833,19 @@ interface Options {
760833
site: string;
761834
}
762835

836+
export const gitRaw: Handler = async (c) => {
837+
const { args } = (await c.req.json()) as GitRawAPI["body"];
838+
839+
const validationError = validateRawArgs(args);
840+
if (validationError) {
841+
return new Response(validationError, { status: 400 });
842+
}
843+
844+
const result = await git.raw(args);
845+
846+
return Response.json({ result });
847+
};
848+
763849
export const checkoutBranch: Handler = async (c) => {
764850
const { branchName } = (await c.req.json()) as CheckoutBranchAPI["body"];
765851

@@ -783,6 +869,7 @@ export const createGitAPIS = (options: Options) => {
783869
app.post("/discard", discard);
784870
app.post("/rebase", rebase);
785871
app.post("/checkout-branch", checkoutBranch);
872+
app.post("/raw", gitRaw);
786873

787874
return app;
788875
};

daemon/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type {
2020
CheckoutBranchAPI,
2121
GitDiffAPI,
2222
GitLogAPI,
23+
GitRawAPI,
2324
GitStatusAPI,
2425
PublishAPI,
2526
RebaseAPI,

0 commit comments

Comments
 (0)