Let gitea_api send only the query keys it needs, never sudo, and never approve a pull request
The confirmation review of 2026-10-09 measured GET /user?sudo=… answer the operator's own account: the query went to the forge unjudged, and the tools' token is the site administrator's, so any caller could act as any user. gitea_api now sends only allow-listed query keys and refuses sudo in any case, and a review that approves or requests changes is the operator's.
This commit is contained in:
@@ -17,6 +17,7 @@ interface Sent { method: string; path: string; body: any }
|
||||
|
||||
const state = {
|
||||
sent: [] as Sent[],
|
||||
asked: [] as string[],
|
||||
statuses: [] as { context: string; state: string }[],
|
||||
rules: [] as any[],
|
||||
lastCommit: "2026-01-01T00:00:00Z",
|
||||
@@ -26,6 +27,7 @@ const protectedMain = () => ({ rule_name: "main", enable_push: false, enable_sta
|
||||
|
||||
beforeEach(() => {
|
||||
state.sent = [];
|
||||
state.asked = [];
|
||||
state.statuses = [{ context: "mesh/merge-gate", state: "success" }, { context: "mesh/repo-check", state: "success" }];
|
||||
state.rules = [protectedMain()];
|
||||
});
|
||||
@@ -46,6 +48,7 @@ const server = createServer(async (req, res) => {
|
||||
const path = url.pathname.replace(/^\/api\/v1/, "");
|
||||
const method = req.method ?? "GET";
|
||||
const body = await read(req);
|
||||
state.asked.push(`${method} ${path}${url.search}`);
|
||||
if (method !== "GET") state.sent.push({ method, path, body });
|
||||
const repo = "/repos/novox/mesh-catalog";
|
||||
if (method === "GET" && path === repo) return json(res, 200, { full_name: "novox/mesh-catalog", default_branch: "main" });
|
||||
@@ -159,7 +162,7 @@ test("gitea_delete_repo deletes only the tools' own user's repository, unguarded
|
||||
});
|
||||
|
||||
test("gitea_api sends the path it judged, and refuses moving a pull request's base", async () => {
|
||||
await tool("gitea_api").run({ path: "/api/v1/repos/novox/mesh-catalog/issues/3/comments/?x=1", method: "POST", body: { body: "hi" } });
|
||||
await tool("gitea_api").run({ path: "/api/v1/repos/novox/mesh-catalog/issues/3/comments/?page=1", method: "POST", body: { body: "hi" } });
|
||||
assert.deepEqual(writes(), ["POST /repos/novox/mesh-catalog/issues/3/comments"]);
|
||||
await assert.rejects(tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/pulls/7", method: "PATCH",
|
||||
body: { base: "unguarded" } }), /changing a pull request's base/);
|
||||
@@ -206,3 +209,27 @@ test("the judges, without a forge", () => {
|
||||
assert.equal(judgeProtectionChange(null, { statusChecks: [] }), null);
|
||||
assert.equal(judgeProtectionChange(protectedMain(), { statusChecks: ["mesh/repo-check", "mesh/merge-gate"] }), null);
|
||||
});
|
||||
|
||||
test("gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused", async () => {
|
||||
for (const path of ["/user?sudo=jschoubben", "/user?SUDO=jschoubben", "/repos/novox/mesh-catalog/issues?state=open&Sudo=jschoubben",
|
||||
"/repos/novox/mesh-catalog/issues?token=abc", "/repos/novox/mesh-catalog/issues?access_token=abc"]) {
|
||||
await assert.rejects(tool("gitea_api").run({ path }), /query key/, path);
|
||||
}
|
||||
await assert.rejects(tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/issues/3/comments?sudo=jschoubben",
|
||||
method: "POST", body: { body: "as the operator" } }), /query key sudo/);
|
||||
assert.deepEqual(state.asked, []);
|
||||
await tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/issues?state=open&limit=5" });
|
||||
assert.deepEqual(state.asked, ["GET /repos/novox/mesh-catalog/issues?state=open&limit=5"]);
|
||||
});
|
||||
|
||||
test("gitea_api never approves a pull request nor requests changes: those are the operator's", async () => {
|
||||
for (const [path, event] of [["/repos/novox/mesh-catalog/pulls/7/reviews", "APPROVE"],
|
||||
["/repos/novox/mesh-catalog/pulls/7/reviews", "approve"], ["/repos/novox/mesh-catalog/pulls/7/reviews", "REQUEST_CHANGES"],
|
||||
["/repos/novox/mesh-catalog/pulls/7/reviews/5", "APPROVE"]]) {
|
||||
await assert.rejects(tool("gitea_api").run({ path, method: "POST", body: { event, body: "lgtm" } }),
|
||||
/operator's at the forge/, `${path} ${event}`);
|
||||
}
|
||||
assert.deepEqual(writes(), []);
|
||||
await tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/pulls/7/reviews", method: "POST", body: { event: "COMMENT", body: "a note" } });
|
||||
assert.deepEqual(writes(), ["POST /repos/novox/mesh-catalog/pulls/7/reviews"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user