Root cause
Other / insufficient public mechanism detail introduced
v0.4.0: 4 new providers, 18 stability fixes, 360 tests
How AI contributed: Direct introductionPublished Jul 3, 2026 in
deepmyst/mystiAll 2 findings in this repository
A vulnerability has been found in DeepMyst Mysti up to 0.4.0. The affected element is the function initProjectMemory of the file src/managers/MemoryManager.ts of the component Per-Project Auto-Memory Handler. Such manipulation of the argument workspacePath leads to exposure of resource. The attack may be performed from remote. Upgrading to version 0.4.0 is sufficient to fix this issue. The name of the patch is 6d709229b5199f6769fb3cf763e5122dcc43c079. It is advisable to upgrade the affected component.
AI-written per-project memory code let a remote caller choose a workspace path and expose project resources.
Root cause
v0.4.0: 4 new providers, 18 stability fixes, 360 tests
Fix
fix: project memory key isolation + legacy dir migration (lands #47) (#49)
Case facts
diff --git a/src/managers/MemoryManager.ts b/src/managers/MemoryManager.tsindex 4244dea..575ad4b 100644--- a/src/managers/MemoryManager.ts+++ b/src/managers/MemoryManager.ts@@ -12,6 +12,7 @@ */ import * as vscode from 'vscode';+import * as crypto from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; import {@@ -30,6 +31,7 @@ import { const GLOBAL_STATE_KEY = 'mysti.autonomousMemory'; const MEMORY_DIR_NAME = 'memory';+const PROJECT_MEMORY_MAX_LINES = 200; interface MemoryStore { entries: MemoryEntry[];@@ -43,6 +45,9 @@ export class MemoryManager { private _syncInterval: NodeJS.Timeout | null = null; private _dirty = false; private _memoryDirPath: string;+ // Per-project auto-memory (like ~/.claude/projects/<project>/memory/)+ private _projectMemoryDir: string | null = null;+ private _projectMemoryContent: string = ''; constructor(context: vscode.ExtensionContext) { this._extensionContext = context;@@ -270,6 +275,160 @@ export class MemoryManager { this._syncToFiles(); } + // ---- Per-Project Auto-Memory (like ~/.claude/projects/<project>/memory/) ----++ /**+ * Initialize per-project memory for the current workspace.+ * Creates ~/.mysti/projects/<hash>/memory/ directory and loads MEMORY.md.+ */+ initProjectMemory(workspacePath: string): void {+ const hash = crypto.createHash('sha256').update(workspacePath).digest('hex').substring(0, 12);+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';+ this._projectMemoryDir = path.join(homeDir, '.mysti', 'projects', hash, 'memory');++ try {+ fs.mkdirSync(this._projectMemoryDir, { recursive: true });+ } catch (error) {+ console.warn('[Mysti] Failed to create project memory dir:', error);+ this._projectMemoryDir = null;+ return;+ }++ this._loadProjectMemory();+ console.log(`[Mysti] Project memory initialized at ${this._projectMemoryDir} (${this._projectMemoryContent.length} chars)`);+ }++ /**+ * Get the first 200 lines of MEMORY.md for prompt injection.+ */+ getProjectMemoryContent(): string {+ return this._projectMemoryContent;+ }++ /**+ * Get the path to the project's MEMORY.md file, or null if not initialized.+ */+ getProjectMemoryPath(): string | null {+ if (!this._projectMemoryDir) { return null; }+ return path.join(this._projectMemoryDir, 'MEMORY.md');+ }++ /**+ * Write or update the MEMORY.md file with new content.+ */+ writeProjectMemory(content: string): void {+ if (!this._projectMemoryDir) { return; }++ const memoryPath = path.join(this._projectMemoryDir, 'MEMORY.md');+ try {+ fs.writeFileSync(memoryPath, content, 'utf-8');+ this._loadProjectMemory(); // Reload cached content (respects 200 line limit)+ } catch (error) {+ console.error('[Mysti] Failed to write project memory:', error);+ }+ }++ /**+ * Write or update a topic file in the project memory directory.+ */+ writeTopicFile(topic: string, content: string): void {+ if (!this._projectMemoryDir) { return; }++ const safeName = topic.replace(/[^a-z0-9-]/gi, '-').toLowerCase() + '.md';+ const filePath = path.join(this._projectMemoryDir, safeName);+ try {+ fs.writeFileSync(filePath, content, 'utf-8');+ } catch (error) {+ console.error(`[Mysti] Failed to write topic file ${safeName}:`, error);+ }+ }++ /**+ * Read a topic file from the project memory directory.+ */+ readTopicFile(topic: string): string | null {+ if (!this._projectMemoryDir) { return null; }++ const safeName = topic.replace(/[^a-z0-9-]/gi, '-').toLowerCase() + '.md';+ const filePath = path.join(this._projectMemoryDir, safeName);+ try {+ if (fs.existsSync(filePath)) {+ return fs.readFileSync(filePath, 'utf-8');+ }+ } catch (error) {+ console.warn(`[Mysti] Failed to read topic file ${safeName}:`, error);+ }+ return null;+ }++ /**+ * Record a project learning. Appends to MEMORY.md under the appropriate section.+ * Creates MEMORY.md with default structure if it doesn't exist.+ */+ recordProjectLearning(section: string, insight: string): void {+ if (!this._projectMemoryDir) { return; }++ const memoryPath = path.join(this._projectMemoryDir, 'MEMORY.md');+ let content: string;++ try {+ content = fs.existsSync(memoryPath) ? fs.readFileSync(memoryPath, 'utf-8') : '';+ } catch {+ content = '';+ }++ // If empty, create default structure+ if (!content.trim()) {+ content = '# Mysti Project Memory\n';+ }++ // Find or create the section+ const sectionHeader = `## ${section}`;+ const sectionIndex = content.indexOf(sectionHeader);++ if (sectionIndex >= 0) {+ // Find the end of this section (next ## or end of file)+ const afterHeader = sectionIndex + sectionHeader.length;+ const nextSectionMatch = content.substring(afterHeader).search(/\n## /);+ const insertPoint = nextSectionMatch >= 0+ ? afterHeader + nextSectionMatch+ : content.length;++ // Check if this insight already exists (dedup)+ const sectionContent = content.substring(afterHeader, insertPoint);+ if (sectionContent.includes(insight)) {+ return; // Already recorded+ }++ // Insert before next section+ content = content.substring(0, insertPoint) + `\n- ${insight}` + content.substring(insertPoint);+ } else {+ // Append new section at end+ content += `\n${sectionHeader}\n\n- ${insight}\n`;+ }++ this.writeProjectMemory(content);+ }++ private _loadProjectMemory(): void {+ if (!this._projectMemoryDir) { return; }++ const memoryPath = path.join(this._projectMemoryDir, 'MEMORY.md');+ try {+ if (fs.existsSync(memoryPath)) {+ const full = fs.readFileSync(memoryPath, 'utf-8');+ // Only load first 200 lines (like Claude Code)+ const lines = full.split('\n');+ this._projectMemoryContent = lines.slice(0, PROJECT_MEMORY_MAX_LINES).join('\n');+ } else {+ this._projectMemoryContent = '';+ }+ } catch (error) {+ console.warn('[Mysti] Failed to load project memory:', error);+ this._projectMemoryContent = '';+ }+ }+ dispose(): void { if (this._syncInterval) { clearInterval(this._syncInterval);diff --git a/src/managers/MemoryManager.ts b/src/managers/MemoryManager.tsindex 575ad4b..81373f1 100644--- a/src/managers/MemoryManager.ts+++ b/src/managers/MemoryManager.ts@@ -34,0 +35 @@ const PROJECT_MEMORY_MAX_LINES = 200;+const PROJECT_MEMORY_KEY_SCHEMA = 'mysti-project-memory-v2';@@ -285 +286 @@ export class MemoryManager {- const hash = crypto.createHash('sha256').update(workspacePath).digest('hex').substring(0, 12);+ const hash = this._getProjectMemoryHash(workspacePath);@@ -287 +288,3 @@ export class MemoryManager {- this._projectMemoryDir = path.join(homeDir, '.mysti', 'projects', hash, 'memory');+ const projectsDir = path.join(homeDir, '.mysti', 'projects');+ this._migrateLegacyProjectMemory(projectsDir, workspacePath, hash);+ this._projectMemoryDir = path.join(projectsDir, hash, 'memory');@@ -412,0 +416,36 @@ export class MemoryManager {+ private _getProjectMemoryHash(workspacePath: string): string {+ const canonicalWorkspacePath = this._getCanonicalWorkspacePath(workspacePath);+ const payload = JSON.stringify({+ schema: PROJECT_MEMORY_KEY_SCHEMA,+ canonicalWorkspacePath,+ });+ return crypto.createHash('sha256').update(payload).digest('hex');+ }++ private _getCanonicalWorkspacePath(workspacePath: string): string {+ try {+ return fs.realpathSync.native(workspacePath);+ } catch {+ return path.resolve(workspacePath);+ }+ }++ private _migrateLegacyProjectMemory(projectsDir: string, workspacePath: string, newHash: string): void {+ try {+ const legacyHash = crypto.createHash('sha256').update(workspacePath).digest('hex').substring(0, 12);+ if (legacyHash === newHash) { return; }+ const legacyDir = path.join(projectsDir, legacyHash);+ const newDir = path.join(projectsDir, newHash);+ if (!fs.existsSync(legacyDir) || fs.existsSync(newDir)) { return; }+ fs.renameSync(legacyDir, newDir);+ console.log(`[Mysti] Migrated legacy project memory ${legacyHash} -> ${newHash.substring(0, 12)}…`);+ } catch (error) {+ console.warn('[Mysti] Legacy project memory migration failed:', error);+ }+ }+AI-assisted change d1b3a18d94630b094f528be37e4450cb536e1b4430f6c4ca7b1b36f96e5df859 · Fix fb4d4bc986890dde04fe995674411ee61cf2537f1374d6cddab4792eec2b03b5