mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-01 09:38:21 +00:00
fix: restrict godmode app launches to godmode callers
`ExecService.launchApp`, reachable from `puter.ui.launchApp`, launched any named app with no check on the target. Gate it: a godmode target may only be launched by a godmode caller. Desktop launches (tile, double-click, URL) call `launch_app` directly and are unaffected; `connectToInstance` already had its own allowlist.
This commit is contained in:
@@ -21,6 +21,17 @@ import { PROCESS_IPC_ATTACHED, Service } from '../definitions.js';
|
||||
import launch_app from '../helpers/launchApp.js';
|
||||
import { expand_home_path } from '../helpers/expandHomePath.js';
|
||||
|
||||
// The /apps endpoint serializes `godmode` as a boolean; older cached shapes used 0/1.
|
||||
const is_godmode = (app_info) =>
|
||||
!! app_info && (app_info.godmode === true || app_info.godmode === 1);
|
||||
|
||||
// `window.get_apps(name)` returns the app object for a single name, or [] when absent.
|
||||
const resolve_app = async (name) => {
|
||||
if ( ! name ) return null;
|
||||
const info = await window.get_apps(name);
|
||||
return info && ! Array.isArray(info) ? info : null;
|
||||
};
|
||||
|
||||
export class ExecService extends Service {
|
||||
static description = `
|
||||
Manages instances of apps on the Puter desktop.
|
||||
@@ -53,6 +64,15 @@ export class ExecService extends Service {
|
||||
const app = ipc_context?.caller?.app;
|
||||
const process = ipc_context?.caller?.process;
|
||||
|
||||
// Only a godmode app may launch another godmode app.
|
||||
const target_app_info = await resolve_app(app_name);
|
||||
if ( is_godmode(target_app_info) ) {
|
||||
const caller_app_info = await resolve_app(process?.name);
|
||||
if ( ! is_godmode(caller_app_info) ) {
|
||||
throw new Error('Launching this app is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
// This mechanism will be replated with xdrpc soon
|
||||
const child_instance_id = window.uuidv4();
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// `definitions.js` reads the class registry the bundle installs at boot.
|
||||
globalThis.def = globalThis.def ?? ((cls) => cls);
|
||||
globalThis.use = globalThis.use ?? (() => ({}));
|
||||
|
||||
// A full launch pulls in UIWindow and the desktop; the gate under test runs
|
||||
// before any of it, so stand the launcher in for a spy.
|
||||
const launchSpy = vi.fn(async () => ({ launchResult: { launched: true } }));
|
||||
vi.mock('../helpers/launchApp.js', () => ({ default: (...a) => launchSpy(...a) }));
|
||||
vi.mock('../helpers/expandHomePath.js', () => ({ expand_home_path: (p) => p }));
|
||||
|
||||
let ExecService;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ ExecService } = await import('./ExecService.js'));
|
||||
});
|
||||
|
||||
// Registry of apps by name, read by the mocked `window.get_apps`.
|
||||
let apps;
|
||||
|
||||
const makeService = async () => {
|
||||
const svc = new ExecService();
|
||||
svc.param_providers = svc.param_providers ?? [];
|
||||
const ipc = {
|
||||
register_ipc_handler: () => {},
|
||||
add_connection: () => ({ forward: { uuid: 'fwd' }, backward: { uuid: 'bwd' } }),
|
||||
};
|
||||
await svc.init({ services: { get: (name) => (name === 'ipc' ? ipc : {}) } });
|
||||
return svc;
|
||||
};
|
||||
|
||||
const callerProcess = (name) => ({
|
||||
name,
|
||||
uuid: `proc-${name}`,
|
||||
references: { iframe: null },
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
launchSpy.mockClear();
|
||||
apps = new Map();
|
||||
globalThis.window = globalThis.window ?? {};
|
||||
let n = 0;
|
||||
window.uuidv4 = () => `uuid-${++n}`;
|
||||
window.get_apps = async (name) => apps.get(name) ?? [];
|
||||
globalThis.puter = { logger: { fields: () => ({ warn () {} }) } };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('ExecService.launchApp godmode gate', () => {
|
||||
it('refuses an ordinary app launching a godmode target', async () => {
|
||||
const svc = await makeService();
|
||||
apps.set('privileged', { name: 'privileged', godmode: true });
|
||||
apps.set('ordinary', { name: 'ordinary', godmode: false });
|
||||
|
||||
await expect(
|
||||
svc.launchApp(
|
||||
{ app_name: 'privileged', args: {} },
|
||||
{ ipc_context: { caller: { app: {}, process: callerProcess('ordinary') } } },
|
||||
),
|
||||
).rejects.toThrow(/not allowed/);
|
||||
expect(launchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// 0/1 is the older serialized shape; it must gate the same as a boolean.
|
||||
it('gates a godmode target expressed as 1 the same way', async () => {
|
||||
const svc = await makeService();
|
||||
apps.set('privileged', { name: 'privileged', godmode: 1 });
|
||||
apps.set('ordinary', { name: 'ordinary', godmode: 0 });
|
||||
|
||||
await expect(
|
||||
svc.launchApp(
|
||||
{ app_name: 'privileged' },
|
||||
{ ipc_context: { caller: { app: {}, process: callerProcess('ordinary') } } },
|
||||
),
|
||||
).rejects.toThrow(/not allowed/);
|
||||
});
|
||||
|
||||
it('lets a godmode caller launch a godmode target', async () => {
|
||||
const svc = await makeService();
|
||||
apps.set('privileged', { name: 'privileged', godmode: true });
|
||||
apps.set('privileged-caller', { name: 'privileged-caller', godmode: true });
|
||||
|
||||
await svc.launchApp(
|
||||
{ app_name: 'privileged' },
|
||||
{ ipc_context: { caller: { app: {}, process: callerProcess('privileged-caller') } } },
|
||||
);
|
||||
expect(launchSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not gate an ordinary target', async () => {
|
||||
const svc = await makeService();
|
||||
apps.set('editor', { name: 'editor', godmode: false });
|
||||
apps.set('ordinary', { name: 'ordinary', godmode: false });
|
||||
|
||||
await svc.launchApp(
|
||||
{ app_name: 'editor' },
|
||||
{ ipc_context: { caller: { app: {}, process: callerProcess('ordinary') } } },
|
||||
);
|
||||
expect(launchSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user