From d0b22ddf864b1afdffff0839ff44f9943266e995 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Fri, 11 Sep 2026 22:05:11 -0300 Subject: [PATCH] fix(open-sse): write Adobe Firefly session tokens and cookie jars with 0700/0600 perms (#12572) (#13242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit. Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them. - ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243) - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK - complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline - 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs - `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243 ⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch. --- .../12572-adobe-firefly-session-file-perms.md | 1 + open-sse/services/adobeFireflyBrowserLogin.ts | 10 +-- open-sse/services/adobeFireflySession.ts | 9 ++- open-sse/utils/secureFileWrite.ts | 25 +++++++ .../adobe-firefly-session-file-perms.test.ts | 73 +++++++++++++++++++ 5 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 changelog.d/fixes/12572-adobe-firefly-session-file-perms.md create mode 100644 open-sse/utils/secureFileWrite.ts create mode 100644 tests/unit/adobe-firefly-session-file-perms.test.ts diff --git a/changelog.d/fixes/12572-adobe-firefly-session-file-perms.md b/changelog.d/fixes/12572-adobe-firefly-session-file-perms.md new file mode 100644 index 0000000000..8030ed697c --- /dev/null +++ b/changelog.d/fixes/12572-adobe-firefly-session-file-perms.md @@ -0,0 +1 @@ +- fix(open-sse): write Adobe Firefly session tokens and cookie jars with 0700/0600 permissions instead of the process umask (#12572) diff --git a/open-sse/services/adobeFireflyBrowserLogin.ts b/open-sse/services/adobeFireflyBrowserLogin.ts index e69884689d..5d58db928c 100644 --- a/open-sse/services/adobeFireflyBrowserLogin.ts +++ b/open-sse/services/adobeFireflyBrowserLogin.ts @@ -14,10 +14,11 @@ */ import { spawn, type ChildProcess } from "node:child_process"; import { createHash } from "node:crypto"; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync } from "node:fs"; import http from "node:http"; import { createServer } from "node:net"; import { join } from "node:path"; +import { ensureSecureDir, writeSecureFile } from "../utils/secureFileWrite.ts"; import { decodeAdobeJwtPayload, isAdobeUserAccessToken, @@ -410,7 +411,7 @@ export function filterAdobeBrowserCookies(cookies: CdpCookie[]): AdobeBrowserCoo function adobeBrowserCookieJarPath(sessionKey: string): string { const dir = join(resolveAdobeFireflyDataRoot(), "adobe-browser-sessions"); - mkdirSync(dir, { recursive: true }); + ensureSecureDir(dir); return join(dir, `${adobeFireflyBrowserSessionKey(sessionKey)}.json`); } @@ -427,10 +428,9 @@ function loadAdobeBrowserCookies(sessionKey: string): AdobeBrowserCookie[] { function saveAdobeBrowserCookies(sessionKey: string, cookies: CdpCookie[]): void { try { - writeFileSync( + writeSecureFile( adobeBrowserCookieJarPath(sessionKey), - JSON.stringify(filterAdobeBrowserCookies(cookies)), - "utf8" + JSON.stringify(filterAdobeBrowserCookies(cookies)) ); } catch { // Best-effort: login still returns the portable JWT + Firefly risk cookies. diff --git a/open-sse/services/adobeFireflySession.ts b/open-sse/services/adobeFireflySession.ts index d8ab034993..552c009d12 100644 --- a/open-sse/services/adobeFireflySession.ts +++ b/open-sse/services/adobeFireflySession.ts @@ -13,8 +13,9 @@ */ import { createHash, randomUUID } from "node:crypto"; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import { ensureSecureDir, writeSecureFile } from "../utils/secureFileWrite.ts"; import { AdobeFireflyError, buildAdobeArpSessionId, @@ -147,7 +148,7 @@ function dataDir(): string { function sessionFilePath(fingerprint: string): string { const dir = join(dataDir(), SESSION_DIR_NAME); try { - if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + ensureSecureDir(dir); } catch { /* ignore */ } @@ -232,7 +233,7 @@ export function markAdobeFireflyArpSuccess(fingerprint: string, arpSessionId: st const obj = JSON.parse(readFileSync(path, "utf8")) as AdobeFireflySession; obj.arpSessionId = arp; obj.updatedAt = Date.now(); - writeFileSync(path, JSON.stringify(obj, null, 2), "utf8"); + writeSecureFile(path, JSON.stringify(obj, null, 2)); sessionCache.set(fp, { ...obj, fingerprint: fp }); } } catch { @@ -455,7 +456,7 @@ function saveDiskSession(session: AdobeFireflySession): void { if (!diskSessionsEnabled()) return; try { const path = sessionFilePath(session.fingerprint); - writeFileSync(path, JSON.stringify(session, null, 2), "utf8"); + writeSecureFile(path, JSON.stringify(session, null, 2)); } catch { /* best-effort */ } diff --git a/open-sse/utils/secureFileWrite.ts b/open-sse/utils/secureFileWrite.ts new file mode 100644 index 0000000000..b076a324c2 --- /dev/null +++ b/open-sse/utils/secureFileWrite.ts @@ -0,0 +1,25 @@ +/** + * Shared helpers for persisting credential/session material (tokens, cookie jars) to disk + * with restrictive permissions — 0700 directories, 0600 files — instead of inheriting the + * process umask (typically 0755/0644). + * + * Mirrors the established pattern in src/lib/vncSession/service.ts::createProfileDir. + * `chmodSync` is applied even on an already-existing directory so a dir created before this + * hardening (or by any looser writer) is tightened rather than silently trusted. + */ + +import { chmodSync, mkdirSync, writeFileSync } from "node:fs"; + +const SECURE_DIR_MODE = 0o700; +const SECURE_FILE_MODE = 0o600; + +/** Create `dir` (recursively) with 0700 permissions, tightening it if it already exists. */ +export function ensureSecureDir(dir: string): void { + mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE }); + chmodSync(dir, SECURE_DIR_MODE); +} + +/** Write `data` to `path` as utf8 with 0600 permissions. */ +export function writeSecureFile(path: string, data: string): void { + writeFileSync(path, data, { encoding: "utf8", mode: SECURE_FILE_MODE }); +} diff --git a/tests/unit/adobe-firefly-session-file-perms.test.ts b/tests/unit/adobe-firefly-session-file-perms.test.ts new file mode 100644 index 0000000000..2c5b2189ce --- /dev/null +++ b/tests/unit/adobe-firefly-session-file-perms.test.ts @@ -0,0 +1,73 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { chmodSync, mkdirSync, mkdtempSync, rmSync, statSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +// Point DATA_DIR at a throwaway tmp dir BEFORE importing the module under test, since +// adobeFireflySession.ts reads process.env.DATA_DIR lazily via dataDir(). +const probeDataDir = mkdtempSync(join(tmpdir(), "adobe-firefly-perm-")); +process.env.DATA_DIR = probeDataDir; + +test.after(() => { + try { + rmSync(probeDataDir, { recursive: true, force: true }); + } catch { + /* best-effort cleanup */ + } +}); + +test("Adobe Firefly session dir/file are created with restrictive permissions (0700/0600)", async () => { + const { markAdobeFireflyArpSuccess, fingerprintAdobeCredential } = await import( + "../../open-sse/services/adobeFireflySession.ts" + ); + + const fp = fingerprintAdobeCredential("probe-credential-blob"); + + // Triggers sessionFilePath() -> ensureSecureDir(dir) with no cached session and no + // pre-existing file on disk (mirrors first-touch creation of the adobe-firefly-sessions + // directory in production). + markAdobeFireflyArpSuccess(fp, "arp-probe-1"); + + const sessionDir = join(probeDataDir, "adobe-firefly-sessions"); + const dirMode = statSync(sessionDir).mode & 0o777; + + assert.equal( + dirMode & 0o077, + 0, + `expected adobe-firefly-sessions dir to be 0700 (no group/other access), got mode ${dirMode.toString(8)}` + ); +}); + +test("ensureSecureDir tightens an already-existing looser directory to 0700", async () => { + const { ensureSecureDir } = await import("../../open-sse/utils/secureFileWrite.ts"); + const dir = join(probeDataDir, "already-loose-dir"); + mkdirSync(dir, { recursive: true, mode: 0o777 }); + chmodSync(dir, 0o777); + + ensureSecureDir(dir); + + const dirMode = statSync(dir).mode & 0o777; + assert.equal( + dirMode & 0o077, + 0, + `expected already-loose-dir to be tightened to 0700, got mode ${dirMode.toString(8)}` + ); +}); + +test("writeSecureFile writes files with 0600 permissions", async () => { + const { writeSecureFile, ensureSecureDir } = await import( + "../../open-sse/utils/secureFileWrite.ts" + ); + const dir = join(probeDataDir, "secure-file-write-probe"); + ensureSecureDir(dir); + const filePath = join(dir, "probe.json"); + writeSecureFile(filePath, JSON.stringify({ hello: "world" })); + + const fileMode = statSync(filePath).mode & 0o777; + assert.equal( + fileMode & 0o177, + 0, + `expected probe.json to be 0600 (no group/other access), got mode ${fileMode.toString(8)}` + ); +});