From 09523dd78fe4ef4ac9e4474d7ab472202bca7cfa Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 28 Sep 2026 20:13:25 -0700 Subject: [PATCH 1/2] test(electron): expect owner-only mode on the token file --- .../src/storage/__tests__/index.test.ts | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/electron/src/storage/__tests__/index.test.ts b/packages/electron/src/storage/__tests__/index.test.ts index 133f5593c45..5156865a36d 100644 --- a/packages/electron/src/storage/__tests__/index.test.ts +++ b/packages/electron/src/storage/__tests__/index.test.ts @@ -1,3 +1,5 @@ +import { chmodSync } from 'node:fs'; + import { safeStorage } from 'electron'; import Store from 'electron-store'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -21,9 +23,14 @@ vi.mock('electron', () => ({ safeStorage: {}, })); +vi.mock('node:fs', () => ({ + chmodSync: vi.fn(), +})); + vi.mock('electron-store', () => ({ default: vi.fn(function () { return { + path: '/tmp/clerk/clerk-tokens.json', get: storeGet, set: storeSet, delete: storeDelete, @@ -72,19 +79,19 @@ describe('storage options', () => { it('creates an electron-store instance with the default store name', () => { storage(); - expect(Store).toHaveBeenCalledWith({ name: 'clerk-tokens' }); + expect(Store).toHaveBeenCalledWith({ name: 'clerk-tokens', configFileMode: 0o600 }); }); it('supports a custom store name', () => { storage({ name: 'custom-clerk-tokens' }); - expect(Store).toHaveBeenCalledWith({ name: 'custom-clerk-tokens' }); + expect(Store).toHaveBeenCalledWith({ name: 'custom-clerk-tokens', configFileMode: 0o600 }); }); it('forwards a custom path as electron-store `cwd`', () => { storage({ path: '/tmp/clerk' }); - expect(Store).toHaveBeenCalledWith({ name: 'clerk-tokens', cwd: '/tmp/clerk' }); + expect(Store).toHaveBeenCalledWith({ name: 'clerk-tokens', configFileMode: 0o600, cwd: '/tmp/clerk' }); }); it('omits `cwd` when no path is provided', () => { @@ -92,6 +99,20 @@ describe('storage options', () => { expect(Store).toHaveBeenCalledWith(expect.not.objectContaining({ cwd: expect.anything() })); }); + + it('restricts an existing token file to owner-only access', () => { + storage(); + + expect(chmodSync).toHaveBeenCalledWith('/tmp/clerk/clerk-tokens.json', 0o600); + }); + + it('does not throw when the token file cannot be chmodded', () => { + vi.mocked(chmodSync).mockImplementationOnce(() => { + throw new Error('ENOENT'); + }); + + expect(() => storage()).not.toThrow(); + }); }); describe('getItem', () => { From 55c603c39b1eb81c52c6263d564ffaaf008d61c6 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 28 Sep 2026 20:13:26 -0700 Subject: [PATCH 2/2] fix(electron): restrict the storage token file to owner-only access --- .changeset/electron-token-file-mode.md | 5 +++++ packages/electron/src/storage/index.ts | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 .changeset/electron-token-file-mode.md diff --git a/.changeset/electron-token-file-mode.md b/.changeset/electron-token-file-mode.md new file mode 100644 index 00000000000..27ef5a2d2e1 --- /dev/null +++ b/.changeset/electron-token-file-mode.md @@ -0,0 +1,5 @@ +--- +'@clerk/electron': patch +--- + +The token file written by `storage()` from `@clerk/electron/storage` is now readable and writable only by its owner (`0600`). It was previously world-writable (`0666`). Existing token files are updated the next time `storage()` is called. diff --git a/packages/electron/src/storage/index.ts b/packages/electron/src/storage/index.ts index 60eb0416497..24146068588 100644 --- a/packages/electron/src/storage/index.ts +++ b/packages/electron/src/storage/index.ts @@ -1,3 +1,5 @@ +import { chmodSync } from 'node:fs'; + import { safeStorage } from 'electron'; import Store from 'electron-store'; @@ -125,8 +127,14 @@ async function resolveCipher(): Promise { export function storage(options: StorageOptions = {}): TokenStorage { const store = new Store>({ name: options.name ?? 'clerk-tokens', + configFileMode: 0o600, ...(options.path ? { cwd: options.path } : {}), }); + try { + chmodSync(store.path, 0o600); + } catch { + /* the file does not exist yet, or it belongs to another user */ + } const memoryFallback = new Map(); // IPC requests can resolve out of order, so only the latest mutation may update storage. const mutationVersions = new Map();