From 33820090cf7e824f69be6738a95bdee0fe44bd5d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:51:45 +0000 Subject: [PATCH 1/2] fix: ensure sensitive files are saved with restrictive permissions Specifically: - Updated `src/api.ts` to save the session token with `0o600` permissions. - Updated `src/app.ts` to save the app configuration (containing appKey) with `0o600` permissions. - Replaced hardcoded `'update.json'` in `src/app.ts` with the `updateJson` constant from `src/utils/constants.ts` for consistency. - Added `fs.chmodSync` calls to ensure existing files also have restricted permissions. - Added a security test in `tests/security.test.ts` to verify the fix. Co-authored-by: sunnylqm <615282+sunnylqm@users.noreply.github.com> --- src/api.ts | 3 +- src/app.ts | 15 ++--- tests/security.test.ts | 140 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 tests/security.test.ts diff --git a/src/api.ts b/src/api.ts index 4c0c8c6..d6c1180 100644 --- a/src/api.ts +++ b/src/api.ts @@ -62,7 +62,8 @@ export const saveSession = () => { if (session !== savedSession) { const current = session; const data = JSON.stringify(current, null, 4); - fs.writeFileSync(credentialFile, data, 'utf8'); + fs.writeFileSync(credentialFile, data, { encoding: 'utf8', mode: 0o600 }); + fs.chmodSync(credentialFile, 0o600); savedSession = current; } }; diff --git a/src/app.ts b/src/app.ts index c7f22fa..8f1cc2f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,6 +4,7 @@ import { question } from './utils'; import { doDelete, get, post } from './api'; import type { Platform } from './types'; +import { updateJson } from './utils/constants'; import { t } from './utils/i18n'; interface AppSummary { @@ -125,9 +126,9 @@ export const appCommands = { let updateInfo: Partial< Record > = {}; - if (fs.existsSync('update.json')) { + if (fs.existsSync(updateJson)) { try { - updateInfo = JSON.parse(fs.readFileSync('update.json', 'utf8')); + updateInfo = JSON.parse(fs.readFileSync(updateJson, 'utf8')); } catch (e) { console.error(t('failedToParseUpdateJson')); throw e; @@ -138,10 +139,10 @@ export const appCommands = { appId: id, appKey, }; - fs.writeFileSync( - 'update.json', - JSON.stringify(updateInfo, null, 4), - 'utf8', - ); + fs.writeFileSync(updateJson, JSON.stringify(updateInfo, null, 4), { + encoding: 'utf8', + mode: 0o600, + }); + fs.chmodSync(updateJson, 0o600); }, }; diff --git a/tests/security.test.ts b/tests/security.test.ts new file mode 100644 index 0000000..61527f5 --- /dev/null +++ b/tests/security.test.ts @@ -0,0 +1,140 @@ +import { + afterEach, + beforeEach, + describe, + expect, + mock, + spyOn, + test, +} from 'bun:test'; + +// Mock missing dependencies BEFORE any imports +mock.module('tty-table', () => ({ + __esModule: true, + default: class Table { + constructor() {} + render() { return 'mocked table'; } + }, +})); +mock.module('i18next', () => ({ + __esModule: true, + default: { + use: () => ({ + init: () => {}, + t: (key: string) => key, + }), + t: (key: string) => key, + }, + t: (key: string) => key, +})); +mock.module('chalk', () => ({ + __esModule: true, + default: { + green: (s: string) => s, + red: (s: string) => s, + yellow: (s: string) => s, + blue: (s: string) => s, + cyan: (s: string) => s, + magenta: (s: string) => s, + }, +})); +mock.module('filesize-parser', () => ({ + __esModule: true, + default: (s: string) => 1024, +})); +mock.module('form-data', () => ({ + __esModule: true, + default: class FormData { + append() {} + }, +})); +mock.module('node-fetch', () => ({ + __esModule: true, + default: mock(async () => ({ + ok: true, + json: async () => ({}), + })), +})); +mock.module('progress', () => ({ + __esModule: true, + default: class ProgressBar { + tick() {} + }, +})); +mock.module('tcp-ping', () => ({ + __esModule: true, + ping: (opts: any, cb: any) => cb(null, { avg: 10 }), +})); +mock.module('fs-extra', () => ({ + __esModule: true, + default: { + ensureDirSync: () => {}, + readFileSync: (f: string, e: string) => '{}', + existsSync: () => true, + }, +})); +mock.module('read', () => ({ + __esModule: true, + read: async () => 'mocked input', +})); +mock.module('compare-versions', () => ({ + __esModule: true, + satisfies: () => true, +})); + +import fs from 'fs'; +import { saveSession, replaceSession } from '../src/api'; +import { appCommands } from '../src/app'; +import { credentialFile } from '../src/utils/constants'; + +// Mock the get function in api.ts +mock.module('../src/api', () => { + const original = require('../src/api'); + return { + ...original, + get: mock(async () => ({ appKey: 'test-app-key' })), + }; +}); + +describe('Security: File Permissions', () => { + const testFiles = [credentialFile, 'update.json', 'cresc.config.json']; + + beforeEach(() => { + // Clean up any existing files + for (const file of testFiles) { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + } + }); + + afterEach(() => { + // Clean up + for (const file of testFiles) { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + } + }); + + test('saveSession should create credentialFile with 0o600 permissions', () => { + replaceSession({ token: 'test-token' }); + saveSession(); + + expect(fs.existsSync(credentialFile)).toBe(true); + const stats = fs.statSync(credentialFile); + expect(stats.mode & 0o777).toBe(0o600); + }); + + test('selectApp should create update.json with 0o600 permissions', async () => { + await appCommands.selectApp({ + args: ['123'], + options: { platform: 'ios' }, + }); + + const targetFile = 'update.json'; + expect(fs.existsSync(targetFile)).toBe(true); + const stats = fs.statSync(targetFile); + expect(stats.mode & 0o777).toBe(0o600); + }); +}); From da83dd2c7075152257be3e4da7b2ae541a784e9c Mon Sep 17 00:00:00 2001 From: Sunny Luo Date: Sun, 5 Apr 2026 09:43:34 +0800 Subject: [PATCH 2/2] Update security.test.ts --- tests/security.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/security.test.ts b/tests/security.test.ts index 61527f5..2ea7140 100644 --- a/tests/security.test.ts +++ b/tests/security.test.ts @@ -12,7 +12,6 @@ import { mock.module('tty-table', () => ({ __esModule: true, default: class Table { - constructor() {} render() { return 'mocked table'; } }, }));