From 34982a598d0ffffc381d8c41ff210757d7ec4b6c Mon Sep 17 00:00:00 2001 From: leemour Date: Fri, 11 Sep 2026 20:22:10 +0200 Subject: [PATCH] fix(store): write the session file with 0600 like the rest of the store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The store deliberately creates config.json, account.json and the account registry with mode 0600 and their directories with 0700. session.json is the one credential file left at the process umask, because mtcute writes it — so on a default umask it lands as 0644. The enclosing directories are 0700, so nothing is exposed today; this is consistency rather than a live hole. It matters for the cases that reach past the directory mode: a store on a shared or copied volume, a backup that preserves file modes but not directory ones, or a umask-relaxed environment. Applied after a successful login, where the file is known to exist. A missing or foreign-owned file is ignored rather than failing the login. --- telegram-client.js | 11 ++++++++++ tests/session-file-mode.test.js | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/session-file-mode.test.js diff --git a/telegram-client.js b/telegram-client.js index 89c4c1d..bfccc7a 100644 --- a/telegram-client.js +++ b/telegram-client.js @@ -918,6 +918,7 @@ class TelegramClient { await this._verifyIdentity(authenticatedUser); } + this._restrictSessionFileMode(); console.log(hasExistingSession ? 'Existing session is valid.' : 'Logged in successfully!'); return true; } catch (error) { @@ -946,6 +947,16 @@ class TelegramClient { } } + // The session file is the account credential, so it gets the same 0600 the store + // already gives config.json and account.json. mtcute writes it with the default umask. + _restrictSessionFileMode() { + try { + fs.chmodSync(this.sessionPath, 0o600); + } catch { + // A missing or foreign-owned session file is not a reason to fail the login + } + } + async ensureLogin() { if (!(await this._isAuthorized())) { throw new Error('Not logged in to Telegram. Please restart the server.'); diff --git a/tests/session-file-mode.test.js b/tests/session-file-mode.test.js new file mode 100644 index 0000000..224413e --- /dev/null +++ b/tests/session-file-mode.test.js @@ -0,0 +1,39 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; + +import TelegramClient from '../telegram-client.js'; + +const dirs = []; +afterEach(() => { + while (dirs.length) fs.rmSync(dirs.pop(), { recursive: true, force: true }); +}); + +const withSessionFile = (mode) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tgcli-session-')); + dirs.push(dir); + const sessionPath = path.join(dir, 'session.json'); + fs.writeFileSync(sessionPath, '{}', { mode }); + fs.chmodSync(sessionPath, mode); + const client = Object.create(TelegramClient.prototype); + client.sessionPath = sessionPath; + return { client, sessionPath }; +}; + +describe('session file permissions', () => { + it('narrows a world-readable session file to 0600', () => { + const { client, sessionPath } = withSessionFile(0o644); + + client._restrictSessionFileMode(); + + expect(fs.statSync(sessionPath).mode & 0o777).toBe(0o600); + }); + + it('does not throw when the session file is absent', () => { + const client = Object.create(TelegramClient.prototype); + client.sessionPath = path.join(os.tmpdir(), 'tgcli-does-not-exist', 'session.json'); + + expect(() => client._restrictSessionFileMode()).not.toThrow(); + }); +});