Skip to content

Commit 0b17389

Browse files
fix(electron): restrict the storage token file to owner-only access (#9971)
Co-authored-by: Mike Pitre <12040919+mikepitre@users.noreply.github.com>
1 parent afe5725 commit 0b17389

3 files changed

Lines changed: 37 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/electron': patch
3+
---
4+
5+
The token file written by `storage()` from `@clerk/electron/storage` is now readable and writable only by its owner (`0600`). It was previously world-readable (`0644` under the default umask). Existing token files are updated the next time `storage()` is called.

‎packages/electron/src/storage/__tests__/index.test.ts‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { chmodSync } from 'node:fs';
2+
13
import { safeStorage } from 'electron';
24
import Store from 'electron-store';
35
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -21,9 +23,14 @@ vi.mock('electron', () => ({
2123
safeStorage: {},
2224
}));
2325

26+
vi.mock('node:fs', () => ({
27+
chmodSync: vi.fn(),
28+
}));
29+
2430
vi.mock('electron-store', () => ({
2531
default: vi.fn(function () {
2632
return {
33+
path: '/tmp/clerk/clerk-tokens.json',
2734
get: storeGet,
2835
set: storeSet,
2936
delete: storeDelete,
@@ -72,26 +79,40 @@ describe('storage options', () => {
7279
it('creates an electron-store instance with the default store name', () => {
7380
storage();
7481

75-
expect(Store).toHaveBeenCalledWith({ name: 'clerk-tokens' });
82+
expect(Store).toHaveBeenCalledWith({ name: 'clerk-tokens', configFileMode: 0o600 });
7683
});
7784

7885
it('supports a custom store name', () => {
7986
storage({ name: 'custom-clerk-tokens' });
8087

81-
expect(Store).toHaveBeenCalledWith({ name: 'custom-clerk-tokens' });
88+
expect(Store).toHaveBeenCalledWith({ name: 'custom-clerk-tokens', configFileMode: 0o600 });
8289
});
8390

8491
it('forwards a custom path as electron-store `cwd`', () => {
8592
storage({ path: '/tmp/clerk' });
8693

87-
expect(Store).toHaveBeenCalledWith({ name: 'clerk-tokens', cwd: '/tmp/clerk' });
94+
expect(Store).toHaveBeenCalledWith({ name: 'clerk-tokens', configFileMode: 0o600, cwd: '/tmp/clerk' });
8895
});
8996

9097
it('omits `cwd` when no path is provided', () => {
9198
storage();
9299

93100
expect(Store).toHaveBeenCalledWith(expect.not.objectContaining({ cwd: expect.anything() }));
94101
});
102+
103+
it('restricts an existing token file to owner-only access', () => {
104+
storage();
105+
106+
expect(chmodSync).toHaveBeenCalledWith('/tmp/clerk/clerk-tokens.json', 0o600);
107+
});
108+
109+
it('does not throw when the token file cannot be chmodded', () => {
110+
vi.mocked(chmodSync).mockImplementationOnce(() => {
111+
throw new Error('ENOENT');
112+
});
113+
114+
expect(() => storage()).not.toThrow();
115+
});
95116
});
96117

97118
describe('getItem', () => {

‎packages/electron/src/storage/index.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { chmodSync } from 'node:fs';
2+
13
import { safeStorage } from 'electron';
24
import Store from 'electron-store';
35

@@ -125,8 +127,14 @@ async function resolveCipher(): Promise<Cipher | null> {
125127
export function storage(options: StorageOptions = {}): TokenStorage {
126128
const store = new Store<Record<string, string>>({
127129
name: options.name ?? 'clerk-tokens',
130+
configFileMode: 0o600,
128131
...(options.path ? { cwd: options.path } : {}),
129132
});
133+
try {
134+
chmodSync(store.path, 0o600);
135+
} catch {
136+
/* the file does not exist yet, or it belongs to another user */
137+
}
130138
const memoryFallback = new Map<string, string>();
131139
// IPC requests can resolve out of order, so only the latest mutation may update storage.
132140
const mutationVersions = new Map<string, number>();

0 commit comments

Comments
 (0)