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(); + }); +});