fix(security): scope managed path opening

This commit is contained in:
ZacharyZhang-NY committed 2026-05-15 00:26:20 -04:00
1 parent bde35c3518
commit 66e24b1060
9 files changed
+66 -30

No files matched your search

+17 -7
View File
@@ -165,7 +165,7 @@ export function buildSettingsMethods(
return purged;
},
canOpenPath(targetPath: string) {
canOpenPath(targetPath: string, companyId: string) {
if (!targetPath || !existsSync(targetPath)) {
return false;
}
@@ -184,15 +184,25 @@ export function buildSettingsMethods(
if (!targetStats.isDirectory() && forbiddenExecutableExtensions.has(targetExtension)) {
return false;
}
if (managedRoots.some((rootPath) => isWithinOrEqual(resolvedTarget, safeRealpath(rootPath)))) {
return true;
}
const workspaces = readAll<{ localPath: string }>("select local_path as localPath from workspaces");
const workspaces = readAll<{ localPath: string }>(
"select local_path as localPath from workspaces where company_id = ?",
companyId,
);
if (workspaces.some((workspace) => isWithinOrEqual(resolvedTarget, safeRealpath(workspace.localPath)))) {
return true;
}
const runLogs = readAll<{ logPath: string | null }>("select log_path as logPath from runs where log_path is not null");
return runLogs.some((run) => run.logPath ? safeRealpath(run.logPath) === resolvedTarget : false);
const runLogs = readAll<{ logPath: string | null }>(
"select log_path as logPath from runs where company_id = ? and log_path is not null",
companyId,
);
if (runLogs.some((run) => run.logPath ? safeRealpath(run.logPath) === resolvedTarget : false)) {
return true;
}
const browserScreenshots = readAll<{ screenshotPath: string | null }>(
"select screenshot_path as screenshotPath from browser_actions where company_id = ? and screenshot_path is not null",
companyId,
);
return browserScreenshots.some((action) => action.screenshotPath ? safeRealpath(action.screenshotPath) === resolvedTarget : false);
},
hasCompany(companyId: string) {
+29 -10
View File
@@ -6,6 +6,7 @@ import {
connectorInputSchema,
connectorModelPoolQuerySchema,
entityGetterSchema,
managedPathInputSchema,
metricsQuerySchema,
settingsSchema,
} from "@shared/contracts";
@@ -57,6 +58,21 @@ export function registerCoreReadHandlers({
| "listConnectorModels"
| "logger"
>) {
function requireCurrentCompanyScope(requestedCompanyId: string) {
const currentCompanyId = db.core.settings.getCurrentCompanyId();
if (!currentCompanyId) {
return { ok: false as const, error: fail("COMPANY_NOT_SELECTED", "Select a company before opening a managed path.") };
}
const companyError = ensureCompanyExists(currentCompanyId);
if (companyError) {
return { ok: false as const, error: companyError };
}
if (requestedCompanyId !== currentCompanyId) {
return { ok: false as const, error: fail("INVALID_COMPANY_REFERENCE", "The requested company must match the current company.") };
}
return { ok: true as const, companyId: currentCompanyId };
}
registerHandle(IPC_CHANNELS.loadSnapshot, () => ok(db.projection.snapshot.listRendererSnapshot()));
registerHandle(IPC_CHANNELS.pickDirectory, async () => {
@@ -66,14 +82,17 @@ export function registerCoreReadHandlers({
return ok(result.canceled ? null : (result.filePaths[0] ?? null));
});
registerHandle(IPC_CHANNELS.openPath, async (_event, targetPath) => {
if (typeof targetPath !== "string" || targetPath.length === 0) {
return fail("INVALID_PATH", "A valid path is required.");
registerHandle(IPC_CHANNELS.openPath, async (_event, payload) => {
const parsed = managedPathInputSchema.safeParse(payload);
if (!parsed.success) {
return fail("INVALID_PAYLOAD", "openPath requires { companyId, path }.");
}
if (!db.core.settings.canOpenPath(targetPath)) {
const currentScope = requireCurrentCompanyScope(parsed.data.companyId);
if (!currentScope.ok) return currentScope.error;
if (!db.core.settings.canOpenPath(parsed.data.path, currentScope.companyId)) {
return fail("OPEN_PATH_FORBIDDEN", "The requested path is outside the managed desktop workspace.");
}
const error = await shell.openPath(targetPath);
const error = await shell.openPath(parsed.data.path);
return error ? fail("OPEN_PATH_FAILED", error) : ok(true);
});
@@ -91,9 +110,9 @@ export function registerCoreReadHandlers({
registerHandle(IPC_CHANNELS.openWorkProduct, async (_event, payload) => {
const parsed = entityGetterSchema.safeParse(payload);
if (!parsed.success) return fail("INVALID_PAYLOAD", "openWorkProduct requires { companyId, id }.");
const companyError = ensureCompanyExists(parsed.data.companyId);
if (companyError) return companyError;
const target = db.core.documents.resolveWorkProductTarget(parsed.data.id, parsed.data.companyId);
const currentScope = requireCurrentCompanyScope(parsed.data.companyId);
if (!currentScope.ok) return currentScope.error;
const target = db.core.documents.resolveWorkProductTarget(parsed.data.id, currentScope.companyId);
if (target.kind === "error") {
const codeByReason = {
not_found: "WORK_PRODUCT_NOT_FOUND",
@@ -104,7 +123,7 @@ export function registerCoreReadHandlers({
path_missing: "WORK_PRODUCT_PATH_MISSING",
} as const;
if (target.reason === "path_missing") {
db.core.documents.updateWorkProduct(parsed.data.id, parsed.data.companyId, { status: "archived" });
db.core.documents.updateWorkProduct(parsed.data.id, currentScope.companyId, { status: "archived" });
publishDomainChanged();
}
return fail(codeByReason[target.reason], target.detail);
@@ -112,7 +131,7 @@ export function registerCoreReadHandlers({
if (target.kind === "url") {
return openExternalUrlWithTrustPolicy(target.value);
}
if (!db.core.settings.canOpenPath(target.value)) {
if (!db.core.settings.canOpenPath(target.value, currentScope.companyId)) {
return fail("OPEN_PATH_FORBIDDEN", "The requested path is outside the managed desktop workspace.");
}
const error = await shell.openPath(target.value);
+6 -1
View File
@@ -135,7 +135,12 @@ export function useCompanyController() {
const openPathWithFeedback = useCallback(async (path: string) => {
const store = useAppStore.getState();
try { unwrap(await window.agentCompany.openPath(path)); store.setError(null); }
const companyId = store.snapshot?.currentCompanyId;
if (!companyId) {
store.setError("Select a company before opening a managed path.");
return;
}
try { unwrap(await window.agentCompany.openPath({ companyId, path })); store.setError(null); }
catch (err) { store.setError(describeDesktopError(err)); }
}, []);
@@ -159,12 +159,7 @@ export function OperationsSections({ section }: OperationsSectionsProps) {
}
}}
onOpenPath={async (path) => {
try {
unwrap(await window.agentCompany.openPath(path));
setError(null);
} catch (error) {
setError(describeDesktopError(error));
}
await openPathWithFeedback(path);
}}
onCancelAction={async (actionId) => {
if (!snapshot?.currentCompanyId) return;
@@ -1,13 +1,13 @@
import type { z } from "zod";
import type { CompanyMetrics, OnboardingBootstrapResult, ProfileSnapshot, StandupReport } from "../../types";
import type { bootstrapOnboardingSchema, companyInputSchema, connectorDisabledModelsSchema, connectorInputSchema, settingsSchema } from "../schemas-company";
import type { bootstrapOnboardingSchema, companyInputSchema, connectorDisabledModelsSchema, connectorInputSchema, managedPathInputSchema, settingsSchema } from "../schemas-company";
import type { entityGetterSchema, metricsQuerySchema } from "../schemas-badges";
import type { DesktopResult } from "./common";
export type CompaniesApi = {
loadSnapshot: () => Promise<DesktopResult<ProfileSnapshot>>;
pickDirectory: () => Promise<DesktopResult<string | null>>;
openPath: (targetPath: string) => Promise<DesktopResult<boolean>>;
openPath: (input: z.infer<typeof managedPathInputSchema>) => Promise<DesktopResult<boolean>>;
openWorkProduct: (input: z.infer<typeof entityGetterSchema>) => Promise<DesktopResult<boolean>>;
saveCompany: (input: z.infer<typeof companyInputSchema>) => Promise<DesktopResult<string>>;
setCurrentCompany: (companyId: string) => Promise<DesktopResult<boolean>>;
+2 -3
View File
@@ -30,6 +30,7 @@ import {
companyInputSchema,
connectorDisabledModelsSchema,
connectorInputSchema,
managedPathInputSchema,
socialAccountInputSchema,
settingsSchema,
workspaceInputSchema,
@@ -213,10 +214,8 @@ export const PRELOAD_FORWARDERS = [
export type PreloadForwardedMethod = (typeof PRELOAD_FORWARDERS)[number]["api"];
export type PreloadForwardedApi = Pick<DesktopApi, PreloadForwardedMethod>;
const managedPathSchema = z.string().min(1);
export const PRELOAD_INPUT_SCHEMAS: Partial<Record<PreloadForwardedMethod, z.ZodType<unknown>>> = {
openPath: managedPathSchema,
openPath: managedPathInputSchema,
openWorkProduct: entityGetterSchema,
saveCompany: companyInputSchema,
setCurrentCompany: companyIdSchema,
+5
View File
@@ -3,6 +3,11 @@ import { optionalId } from "./helpers";
export const companyIdSchema = z.string().uuid();
export const managedPathInputSchema = z.object({
companyId: companyIdSchema,
path: z.string().min(1),
});
export const companyInputSchema = z.object({
id: optionalId,
name: z.string().min(2).max(80),
@@ -33,6 +33,7 @@ function registerOpenWorkProductHandler(url: string) {
},
settings: {
canOpenPath: vi.fn(() => true),
getCurrentCompanyId: vi.fn(() => companyId),
},
},
projection: {
+3 -1
View File
@@ -105,7 +105,9 @@ describe("preload forwarders", () => {
expect(objectArg).toBeDefined();
expect(mapPreloadInvokeArgs(noArgs!.payload, [])).toEqual([]);
expect(mapPreloadInvokeArgs(singleArg!.payload, ["/tmp/output"])).toEqual(["/tmp/output"]);
expect(mapPreloadInvokeArgs(singleArg!.payload, [{ companyId: "2f88b7d1-dafb-42f1-ac75-34fc2de3b0f4", path: "/tmp/output" }])).toEqual([
{ companyId: "2f88b7d1-dafb-42f1-ac75-34fc2de3b0f4", path: "/tmp/output" },
]);
expect(mapPreloadInvokeArgs(objectArg!.payload, ["company-1", "/skills/local"])).toEqual([
{ companyId: "company-1", source: "/skills/local" },
]);