From b0e60cfad23382c088015faab08043a589414c78 Mon Sep 17 00:00:00 2001 From: jeffyxu Date: Mon, 24 Aug 2026 15:20:34 +0800 Subject: [PATCH] fix(home): fail loud instead of falling back to os.tmpdir in getUserHome Follow-up to #309. That PR ended getUserHome()'s fallback chain with `|| os.tmpdir()` to guarantee a non-empty path. But callers join the result onto `.teamai/...` to write credentials (api-key.ts) and to resolve and execute a node binary (builtin-hooks.ts resolveCodebuddyNode/resolveWorkbuddyNode), so falling back to a world-writable shared directory is a code-execution and credential-exposure vector: an attacker who can write /tmp/.codebuddy-server/.../node would get their binary run. Throw when none of HOME, USERPROFILE, or os.homedir() resolves instead. The empty-string case os.tmpdir() was guarding against (cwd-relative paths) is also covered by throwing. On any real machine at least one of the three is set, so this path is only reached in pathological contexts (passwd-less uid, `env -i`) where refusing to run is the correct, safe behavior. Update the regression test to assert the throw. Co-Authored-By: Claude Fable 5 --- src/__tests__/home.test.ts | 8 ++++---- src/utils/home.ts | 28 +++++++++++++++++++--------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/__tests__/home.test.ts b/src/__tests__/home.test.ts index 4da8376..309bcbf 100644 --- a/src/__tests__/home.test.ts +++ b/src/__tests__/home.test.ts @@ -70,16 +70,16 @@ describe('getUserHome', () => { expect(getUserHome()).toBe(os.homedir()); }); - it('falls back to os.tmpdir when even os.homedir is unresolvable', () => { + it('throws instead of returning a shared/relative dir when home is unresolvable', () => { delete process.env.HOME; delete process.env.USERPROFILE; // os.homedir() is documented to return '' when the home cannot be resolved. + // Falling back to os.tmpdir() (world-writable) or '' (cwd-relative) would be a + // credential-exposure / code-execution vector, so getUserHome() must throw. const spy = vi.spyOn(os, 'homedir').mockReturnValue(''); try { - const home = getUserHome(); - expect(home).toBe(os.tmpdir()); - expect(home).not.toBe(''); + expect(() => getUserHome()).toThrow(/user home directory/); } finally { spy.mockRestore(); } diff --git a/src/utils/home.ts b/src/utils/home.ts index fba4286..cf4a034 100644 --- a/src/utils/home.ts +++ b/src/utils/home.ts @@ -4,16 +4,26 @@ import os from 'node:os'; * Resolve the current user's home directory across supported platforms. * * `HOME` is normally present on Unix-like systems, while a regular Windows - * PowerShell session commonly exposes only `USERPROFILE`. `os.homedir()` is - * the platform-aware fallback, and `os.tmpdir()` is a last resort: `os.homedir()` - * is documented to return `''` when the home cannot be resolved (e.g. a passwd-less - * uid or `env -i`), and callers join this onto `.teamai/...`, so an empty result - * would silently yield a cwd-relative path. Guaranteeing a non-empty absolute path - * keeps every derived path absolute. + * PowerShell session commonly exposes only `USERPROFILE`. `os.homedir()` is the + * platform-aware fallback. + * + * If none of these resolves (os.homedir() is documented to return '' for a + * passwd-less uid or under `env -i`), we throw rather than fall back to a + * shared/relative directory. Callers join this onto `.teamai/...` to write + * credentials and to resolve executables, so a shared location like os.tmpdir() + * would be a code-execution / credential-exposure vector, and an empty string + * would silently produce a cwd-relative path. Failing loudly is the safe choice — + * on any real machine at least one of the three is always set. */ export function getUserHome(): string { - return process.env.HOME?.trim() + const home = process.env.HOME?.trim() || process.env.USERPROFILE?.trim() - || os.homedir() - || os.tmpdir(); + || os.homedir(); + if (!home) { + throw new Error( + 'Unable to determine the user home directory: none of HOME, USERPROFILE, ' + + 'or os.homedir() is available. Set HOME explicitly before running teamai.', + ); + } + return home; }