Unauthenticated Path Traversal Leads to Recursive Deletion of SillyTavern Extensions Directory
POST /api/extensions/delete accepts extensionName: ".", which sanitize-filename reduces to an empty string, collapsing the target path to the base directory before an unguarded recursive fs.rm
SillyTavern, a self-hosted local UI for LLM/image-gen/TTS backends, is vulnerable prior to version 1.18.0 to an unauthenticated path traversal in its extension-management API. The POST /api/extensions/delete endpoint validates extensionName before sanitizing it; submitting a single dot (".") survives validation, gets reduced to an empty string by the sanitization library, and collapses the delete target to the base extensions directory. Because SillyTavern ships with no authentication by default, any network-reachable attacker can send one request and permanently, recursively delete the entire extensions directory — with no confidentiality impact but full integrity/availability loss. Fixed in 1.18.0.
Technical Breakdown
Unauthenticated Path Traversal Leads to Recursive Deletion of SillyTavern Extensions Directory
The /api/extensions/delete handler checks that extensionName is truthy before passing it to the sanitize-filename library, rather than after. A single dot "." is truthy and passes validation, but sanitize-filename normalizes it down to an empty string. path.join(extensionsBase, "") then resolves to the extensions root itself instead of a subdirectory, and the subsequent fs.promises.rm(extensionPath, { recursive: true }) call recursively deletes the entire extensions tree in one unauthenticated request.
basicAuthMode: true in config.yaml as a mitigation, not a fix.
Root Cause
Order-of-operations bug: the handler's input validation runs before sanitization instead of after. Truthiness is checked on the raw, attacker-controlled string, but the string that actually reaches the filesystem is the sanitized one — and sanitization can turn a non-empty, "valid-looking" value into an empty one. Nothing downstream re-validates the sanitized result before it's joined into a filesystem path.
Vulnerable Pattern
Any code path that does if (input) { sanitize(input) } and then trusts the sanitized output to still be a meaningful subpath is exposed. Sanitization libraries are built to strip dangerous characters, not to guarantee a non-empty result — ".", "..", and whitespace-only inputs are exactly the class of values that collapse to empty strings after normalization.
Why AI Services Are High-Value
SillyTavern is a local front-end for LLM, image-gen, and TTS backends, and its extension ecosystem is how operators wire in additional AI tooling and integrations. It ships with no authentication by default because it's designed to run on localhost — but that "local-only" trust model breaks the moment the app is port-forwarded, run in a shared lab environment, or reached via a victim's browser through DNS rebinding, turning a convenience default into an open, unauthenticated destruction primitive.
The Fix
Version 1.18.0 sanitizes extensionName before validating truthiness, so a value that collapses to an empty string after sanitization is correctly rejected instead of being joined into the base extensions path. Upgrading is the only complete remediation.
Attack Chain
Recon — Fingerprint exposed SillyTavern instances
Attacker scans for SillyTavern's default port/banner (commonly 8000) via Shodan/Censys or targeted sweeps, or targets a specific victim's LAN-bound instance via a malicious webpage using DNS rebinding (CVE-2025-59159) to pivot browser-origin requests onto 127.0.0.1.
Defensive note: Never expose SillyTavern to the public internet; monitor egress/ingress for unexpected inbound connections to the app's port; treat unsolicited scan traffic as a signal to review firewall rules.
Confirm no-auth posture
Attacker probes a benign endpoint (e.g. /csrf-token, /api/extensions/list) without credentials to confirm basicAuthMode is disabled (the default).
Defensive note: Audit config.yaml for basicAuthMode: false across all deployed instances; alert on any instance reachable without an auth challenge.
Craft and send the malicious delete request
Attacker fetches a CSRF token (trivially available pre-auth) and issues POST /api/extensions/delete with body {"extensionName": "."}.
Defensive note: Reverse-proxy/WAF rule to flag or block extensionName values that are ".", "..", empty, or contain path-separator-adjacent characters before they reach the app; log all requests to /api/extensions/* with their raw body.
Server-side path collapse and recursive deletion
The truthy-but-unsanitized "." passes validation, sanitize-filename reduces it to "", path.join(extensionsBase, "") resolves to the extensions root, and fs.promises.rm(..., { recursive: true }) wipes the entire directory tree in one call.
Defensive note: File-integrity monitoring (auditd/inotify watches) on the extensions directory to catch mass-delete events in real time; ensure the process user has least-privilege filesystem access rather than broad write/delete rights outside its intended scope.
Impact — Irrecoverable loss / service disruption
All installed third-party extensions are permanently gone unless backed up; the app may become unstable or lose functionality; if chained with DNS rebinding, this is achievable purely by a victim visiting a malicious webpage while SillyTavern runs locally.
Defensive note: Maintain out-of-band backups of the extensions directory so recovery doesn't depend on the vulnerable app itself; also patch/mitigate CVE-2025-59159 (DNS rebinding), the realistic remote delivery mechanism for an otherwise "local-only" app.
Impact
Destructive, unauthenticated primitives are disproportionately damaging in self-hosted AI tooling — there's no admin console, no built-in backup subsystem, and often no operator watching until the interface silently breaks.
Data Integrity / Availability
- Entire extensions directory recursively and permanently deleted with a single unauthenticated request.
- No confidentiality impact — this is a pure destruction primitive, not data theft.
- Custom/self-written extensions with no upstream source are unrecoverable without local backups.
- Trivially scriptable for mass disruption against multiple discovered instances.
Service Continuity / Operational Disruption
- Loss of extensions can break dependent workflows — custom UI panels, integrations, TTS/image-gen bridges.
- Recovery requires manual reinstallation and reconfiguration of every extension.
- No built-in restore mechanism exists in the application itself.
- For SillyTavern's largely hobbyist audience, this can mean total loss of a customized setup with no recovery path.
Remote Exploitability via Chaining (Local-Only Assumption Broken)
- The "local-only" threat model (no-auth by default) is defeated when combined with CVE-2025-59159 DNS rebinding.
- A malicious website can trigger this from a victim's browser against their own local instance.
- Demonstrates that "runs on localhost" is an implicit trust boundary that fails against browser-based pivoting.
- Other endpoints sharing the same validate-before-sanitize pattern (/api/extensions/update, /version, /branches, /switch) are candidates for the same bug class.
Defensive Tutorial
Upgrade to 1.18.0+
ImmediateThe only complete fix. Confirm via git log/package version or the app's About screen that the running instance is >= 1.18.0.
Enable basicAuthMode: true
Immediate
Set this in config.yaml on any instance that cannot be upgraded immediately — closes the no-auth precondition this exploit depends on, even though it doesn't fix the underlying path-traversal logic.
Grep logs for exploitation attempts
ImmediateSearch access/reverse-proxy logs for POST /api/extensions/delete with body containing "extensionName":"." (or %2E, ..%2F variants) to determine if the instance was already hit.
Snapshot the extensions directory now
ImmediateBack up data/<user>/extensions (or equivalent) before patching, in case recovery is needed and to establish a clean baseline post-patch.
Bind the service to 127.0.0.1 only
ImportantDo not bind to 0.0.0.0 unless remote access is a deliberate, authenticated requirement — check the listen/whitelist settings in config.yaml.
Front any exposed instance with an authenticating reverse proxy
Importantnginx/Caddy + basic auth or SSO in front of the app so unauthenticated requests never reach the Node backend at all.
Add a WAF/proxy rule blocking suspicious extensionName payloads
ImportantBlock ".", "..", empty string, and leading "/" on /api/extensions/* routes as defense-in-depth while broader patch rollout completes.
Track SillyTavern in an SBOM/asset inventory
ImportantInclude personal dev machines and lab environments so future advisories (this vendor has had multiple CVEs in 2025-2026: DNS rebinding, CORS XSS, this path traversal) trigger automatic patch alerts.
Adopt a "validate-after-sanitize, always" code review rule
ImportantThis bug class (order-of-operations between validation and sanitization) is exactly what a security-focused code review checklist should catch before merge; the advisory itself flagged sibling endpoints using the same flawed pattern as worth auditing.
References
- NVD Entry CVE-2026-44650 — National Vulnerability Database record, CVSS 9.1.
- GitHub Security Advisory GHSA-886q-f44j-h6wh — vendor disclosure and researcher writeup.
- Vendor Fix Release SillyTavern 1.18.0 — release containing the validate-after-sanitize fix.
- CWE Reference CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')