From b7bb81c543d11d35ef3995e9af9cc7918b0d369b Mon Sep 17 00:00:00 2001 From: Amolith Date: Sun, 5 Apr 2026 19:49:44 -0600 Subject: [PATCH] personality: Harden filesystem ops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap all filesystem calls (readdirSync, readFileSync, writeFileSync, unlinkSync) in try/catch with graceful fallbacks — return empty/null instead of crashing the extension. Validate the persisted personality name against listPersonalities() before loading it. A tampered persistence file containing a path like ../../etc/something would previously be passed straight to readFileSync; now it is rejected if not in the available list. --- packages/personality/src/index.ts | 58 ++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/packages/personality/src/index.ts b/packages/personality/src/index.ts index f0a1dc49dde876abf86babf5158e361a4fd1d963..b9c18d292a1367beaacc27685f54ae2b0726ae33 100644 --- a/packages/personality/src/index.ts +++ b/packages/personality/src/index.ts @@ -49,29 +49,56 @@ function listPersonalities(): string[] { const dir = getPersonalitiesDir(); if (!fs.existsSync(dir)) return []; - return fs - .readdirSync(dir) - .filter((f) => f.endsWith(".md")) - .map((f) => f.slice(0, -3)) - .sort(); + try { + return fs + .readdirSync(dir) + .filter((f) => f.endsWith(".md")) + .map((f) => f.slice(0, -3)) + .sort(); + } catch (err) { + console.error(`Failed to list personalities: ${err}`); + return []; + } } -/** Read the persisted active personality name, or null if none. */ +/** + * Read the persisted active personality name, or null if none. + * Validates the name against available personalities to prevent + * path traversal via a tampered persistence file. + */ function readActivePersonality(): string | null { const filePath = getActivePersonalityPath(); if (!fs.existsSync(filePath)) return null; - const name = fs.readFileSync(filePath, "utf-8").trim(); - return name || null; + try { + const name = fs.readFileSync(filePath, "utf-8").trim(); + if (!name) return null; + + // Reject names that could escape the personalities directory + const available = listPersonalities(); + if (!available.includes(name)) { + console.error(`Persisted personality "${name}" not in available list, ignoring`); + return null; + } + + return name; + } catch (err) { + console.error(`Failed to read active personality: ${err}`); + return null; + } } /** Persist the active personality name (or remove the file to clear). */ function writeActivePersonality(name: string | null): void { const filePath = getActivePersonalityPath(); - if (name) { - fs.writeFileSync(filePath, `${name}\n`, "utf-8"); - } else if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); + try { + if (name) { + fs.writeFileSync(filePath, `${name}\n`, "utf-8"); + } else if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } catch (err) { + console.error(`Failed to write active personality: ${err}`); } } @@ -79,7 +106,12 @@ function writeActivePersonality(name: string | null): void { function readPersonalityContent(name: string): string | null { const filePath = path.join(getPersonalitiesDir(), `${name}.md`); if (!fs.existsSync(filePath)) return null; - return fs.readFileSync(filePath, "utf-8"); + try { + return fs.readFileSync(filePath, "utf-8"); + } catch (err) { + console.error(`Failed to read personality "${name}": ${err}`); + return null; + } } const PERSONALITY_TAG_OPEN =