fix(agent-api): reject placeholder/weak AGENT_API_TOKEN as unconfigured

.env.example ships AGENT_API_TOKEN=replace-me, and unlike PB_SUPERUSER_PASSWORD's
own replace-me placeholder, an unchanged agent token fails open: any caller
sending "Bearer replace-me" would get full owner-equivalent access. Treat a
secret that is unset, literally "replace-me", or shorter than 32 characters
as not configured, matching the existing 500 behavior for an unset token.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Dave 2026-09-26 10:23:07 +02:00
parent cf942d67c7
commit 8b55a10c02
2 changed files with 32 additions and 6 deletions

View file

@ -20,7 +20,7 @@ describe("requireAgentToken", () => {
});
it("refuses a request with no Authorization header", () => {
process.env.AGENT_API_TOKEN = "secret123";
process.env.AGENT_API_TOKEN = "secret123secret123secret123secret123";
const request = new Request("http://localhost/api/agent/projects");
const result = requireAgentToken(request);
@ -30,7 +30,7 @@ describe("requireAgentToken", () => {
});
it("refuses a request with the wrong token", () => {
process.env.AGENT_API_TOKEN = "secret123";
process.env.AGENT_API_TOKEN = "secret123secret123secret123secret123";
const request = new Request("http://localhost/api/agent/projects", {
headers: { authorization: "Bearer wrong" },
});
@ -42,13 +42,37 @@ describe("requireAgentToken", () => {
});
it("allows a request with the exact right token", () => {
process.env.AGENT_API_TOKEN = "secret123";
process.env.AGENT_API_TOKEN = "secret123secret123secret123secret123";
const request = new Request("http://localhost/api/agent/projects", {
headers: { authorization: "Bearer secret123" },
headers: { authorization: "Bearer secret123secret123secret123secret123" },
});
const result = requireAgentToken(request);
expect(result).toBeNull();
});
it("refuses with 500 when the token is the literal placeholder, even with a matching header", () => {
process.env.AGENT_API_TOKEN = "replace-me";
const request = new Request("http://localhost/api/agent/projects", {
headers: { authorization: "Bearer replace-me" },
});
const result = requireAgentToken(request);
expect(result).not.toBeNull();
expect(result!.status).toBe(500);
});
it("refuses with 500 when the token is shorter than 32 characters, even with a matching header", () => {
process.env.AGENT_API_TOKEN = "short";
const request = new Request("http://localhost/api/agent/projects", {
headers: { authorization: "Bearer short" },
});
const result = requireAgentToken(request);
expect(result).not.toBeNull();
expect(result!.status).toBe(500);
});
});

View file

@ -15,8 +15,10 @@ import { jsonError } from "@/lib/http/json";
*/
export function requireAgentToken(request: Request): Response | null {
const secret = process.env.AGENT_API_TOKEN;
if (!secret) {
console.error("[agent-api] AGENT_API_TOKEN is not set; refusing all requests");
if (!secret || secret === "replace-me" || secret.length < 32) {
console.error(
"[agent-api] AGENT_API_TOKEN is not set to a real secret; refusing all requests"
);
return jsonError("Not configured", 500);
}