GitHub Advisoryadvisory · npm
GHSA-9hc2-hjx8-q6pv TidGi Desktop Remote Code Execution via Malicious TiddlyWiki Repository Import — Tiddler Startup Module Auto-Execution
CRITICAL
Official page ↗
- Ecosystem
- npm
- Packages
- tidgi
- Severity
- Critical
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
- Weaknesses
- CWE-94
- Reviewed
- 2026-07-14
Affected packages
• npm tidgi: <= 0.13.0 (no fix yet)
Description
Description
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:
1. 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()`.
2. 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.
3. 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.
Affected Product
• Product: TidGi Desktop
• Vendor: Lin Onetwo (https://github.com/tiddly-gittly)
• Repository: https://github.com/tiddly-gittly/TidGi-Desktop
• Affected Versions: 0.13.0 (latest release)
• 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)
• Package: tidgi (npm)
Vulnerability Details
Root Cause 1 — `.tid` Files Auto-Loaded Before Module Processing
File: `src/services/wiki/wikiWorker/loadWikiTiddlersWithSubWikis.ts:59-92`
```typescript
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`
```typescript
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.
Root Cause 2 — Automatic Module Registration via `module-type` Field
File: `node_modules/tiddlywiki/boot/boot.js:1514-1534` — `defineTiddlerModules()`
```javascript
$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.
Root Cause 3 — Automatic Startup Execution with Full Node.js Access
File: `node_modules/tiddlywiki/boot/boot.js:2572-2576` — Collecting startup modules
```javascript
$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
```javascript
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`)
```javascript
$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
};
```
Complete Boot Sequence (verified against TiddlyWiki 5.4.0)
```
$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
```
Exploitation Conditions
• No authentication required — importing a wiki is a standard feature
• Only user interaction: click "Add Workspace" → select folder/URL → confirm
Complete Attack Flow
```
┌──────────────────────────────────────────────────────────────┐
│ 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() │
│ defi…
Related
Weakness · CWE (1)
- CWE-94 Improper Control of Generation of Code weakness