personality: Harden filesystem ops
Amolith
created 1 week ago
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.
Change summary
packages/personality/src/index.ts | 58 +++++++++++++++++++++++++-------
1 file changed, 45 insertions(+), 13 deletions(-)
Detailed changes
@@ -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 =