fix(security): deny unknown company board-key routes

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-05 04:24:03 +00:00 committed by cryppadotta
parent 6ccb228f93
commit 09f5f1ce2e
2 changed files with 47 additions and 5 deletions

View File

@ -58,6 +58,8 @@ describe("board-key route registry", () => {
["GET", "/api/cli-auth/me", "deny", "board_key_denied"],
["POST", "/runtime-tools/connections/request", "deny", "board_key_denied"],
["POST", "/api/connection-intents/11111111-1111-4111-8111-111111111111/complete", "deny", "board_key_denied"],
["GET", "/api/companies/11111111-1111-4111-8111-111111111111/not-yet-registered", "deny", "board_key_denied"],
["POST", "/api/companies/11111111-1111-4111-8111-111111111111/not-yet-registered/search", "deny", "board_key_denied"],
["GET", "/api/not-yet-registered", "deny", "undeclared"],
] as const)("classifies %s %s", (method, routePath, action, classification) => {
expect(lookupBoardKeyRoute(method, routePath)).toMatchObject({ action, classification });

View File

@ -151,9 +151,13 @@ function queryCompanyId(rawPath: string) {
}
}
function companySubresourcePermission(method: string, subresource: string | undefined, tail: string[]) {
function companySubresourcePermission(
method: string,
subresource: string | undefined,
): BoardPermissionKey | null {
const write = !SAFE_METHODS.has(method);
switch (subresource) {
case undefined: return write ? "companies:write" : "companies:read";
case "agents":
case "org":
return write ? "agents:write" : "agents:read";
@ -204,9 +208,44 @@ function companySubresourcePermission(method: string, subresource: string | unde
case "import":
case "export":
case "exports": return "companies:import_export";
default:
if (tail.some((segment) => segment === "search" || segment === "extract")) return "search:read";
return write ? "companies:write" : "companies:read";
// These company-owned surfaces intentionally retain their reviewed
// companies:read/write behavior. Listing them makes future subresources
// fail closed instead of inheriting that authority from a default branch.
case "archive":
case "attention":
case "branding":
case "budget-incidents":
case "case-events":
case "cases":
case "claude-oauth-token-status":
case "cost-events":
case "dashboard":
case "decision-archive-proposals":
case "decision-bundles":
case "decision-queue-seed-rules":
case "imports":
case "live-runs":
case "logo":
case "managed-agent-profiles":
case "onboarding-seed":
case "org.png":
case "org.svg":
case "pipelines":
case "pipelines-attention":
case "provider-traces":
case "recovery-observability":
case "remote-agent-profiles":
case "review-cases":
case "secret-proposals":
case "secret-providers":
case "setup-token-login-sessions":
case "sidebar-badges":
case "smoke-lab":
case "timeline":
case "user-directory":
case "user-secret-definitions":
case "workspace-overview": return write ? "companies:write" : "companies:read";
default: return null;
}
}
@ -363,7 +402,8 @@ export function lookupBoardKeyRoute(methodInput: string, rawPath: string): Board
if (isDeniedCompanyRoute(method, segments.slice(3))) {
return denied(method, `/api/companies/${ID}/${segments.slice(3).join("/")}`);
}
const action = companySubresourcePermission(method, segments[3], segments.slice(4));
const action = companySubresourcePermission(method, segments[3]);
if (!action) return denied(method, `/api/companies/${ID}/{*path}`);
return declared(method, `/api/companies/${ID}/{*path}`, action, "company", "company", {
companyId,
concealment: segments.length > 3 ? "not_found" : "forbidden",