# WebDAV share-root containment bypass on webdav-*.alwaysdata.net (WsgiDAV 4.3.3 / CVE-2026-48099) ## Summary The per-account WebDAV service at `https://webdav-.alwaysdata.net/` runs **WsgiDAV 4.3.3** (disclosed in its own error pages). That version is affected by **CVE-2026-48099** — "Encoded dot segments can escape WsgiDAV filesystem share roots" — fixed in WsgiDAV 4.3.4. `FilesystemProvider._loc_to_file_path()` in 4.3.3 confines a request to the account's home directory with a **string-prefix check** (`file_path.startswith(root_path)`) rather than a real path-boundary check. As a result a request path that resolves *outside* the configured share root is accepted as long as the resulting absolute path merely **starts with the root path string**. I confirmed this containment bypass is live on the alwaysdata WebDAV fleet, on two independent backend servers. Home directories on the shared servers are laid out as `/home/` and account names are chosen freely at signup, so the practical consequence is cross-tenant file access (read / write / delete) between accounts that (a) reside on the same physical server and (b) where the attacker's account name is a string-prefix of the victim's — both of which an attacker controls through ordinary free signups. ## Affected asset (in scope) - `https://webdav-.alwaysdata.net/` - Software: `WsgiDAV/4.3.3` behind `gunicorn` (banner in every 404/500 page) - Auth: HTTP Digest, `realm="alwaysdata"`. This is **not** an authentication bypass — the attacker uses their own (free) account's WebDAV credentials. ## Root cause (public advisory) CVE-2026-48099 / GHSA-wxq4-cc2q-338q (WsgiDAV `<= 4.3.3`, fixed 4.3.4): > The method builds a candidate path with > `os.path.abspath(os.path.join(root_path, *path_parts))`, then checks > containment with `file_path.startswith(root_path)`. This is not path-boundary > aware. For example, if the configured share root is `/tmp/share`, a resolved > sibling path such as `/tmp/share_evil/secret.txt` still starts with the string > `/tmp/share`. In a local proof, this allowed **GET, PUT, and DELETE** requests > to operate on files outside the configured share root. > The WSGI/server layer forwards the encoded dot segment to WsgiDAV's PATH_INFO > (`/%2e%2e/...` -> `/../...`). ## Proof of Concept (observed on alwaysdata) Two of my own free accounts, on two different backend servers: - `mehdik5100a` — WebDAV root `/home/mehdik5100a` - `mehdik5100` — WebDAV root `/home/mehdik5100` The oracle: a request whose resolved path leaves the root **but keeps the root as a string prefix** should be rejected (`500 "outside root"`) by a path-boundary-aware server. On WsgiDAV 4.3.3 it is **accepted** and only returns `404` when the target simply does not exist on disk. ``` ### Version banner $ bbcurl --digest -u 'mehdik5100a:***' https://webdav-mehdik5100a.alwaysdata.net/nonexistent-xyz -i HTTP/2 404 server: gunicorn WsgiDAV/4.3.3 - 2026-09-25 06:55:25 ### Server A (root = /home/mehdik5100a) PROPFIND /..%2f -> 500 "Security exception: tried to access file outside root: /home" PROPFIND /..%2f..%2f -> 500 "...outside root: /" PROPFIND /..%2fmehdik5100a/www/ -> 207 (escape resolves back INTO own home; directory listed) PROPFIND /..%2fmehdik5100aZZZ/ -> 404 <-- NOT 500 ### Server B (root = /home/mehdik5100, independent backend, same result) PROPFIND /..%2f -> 500 "...outside root: /home" PROPFIND /..%2fmehdik5100ZZZ/ -> 404 <-- NOT 500 ``` Interpretation of the smoking-gun line: - `/..%2fmehdik5100aZZZ/` resolves to `/home/mehdik5100aZZZ`, which is **outside** the share root `/home/mehdik5100a`. - A correct boundary check returns `500 "outside root"` — exactly what `/..%2f` (`/home`) and `/..%2f..%2f` (`/`) return. - Instead it returns `404`, i.e. `"/home/mehdik5100aZZZ".startswith("/home/mehdik5100a")` is `True`, the containment check passed, and the only reason for the 404 is that no directory of that exact name exists on that server. Any path `/home/mehdik5100a*` therefore passes containment. Per the CVE, the same resolver is used for `GET`, `PUT`, and `DELETE`. ## From containment bypass to cross-tenant compromise Because home dirs are `/home/` and names are attacker-chosen: 1. To read/write/delete a specific customer `victim`'s files, an attacker registers a free account whose name is a prefix of `victim` (e.g. `victim` minus its last character), so that `/home/victim` starts with `/home/`. 2. The attacker's account must land on the same physical server as the victim. Free signups are spread across a finite server pool, so an attacker retries signup until co-located (or, opportunistically, registers a short prefix and harvests every co-located account whose name begins with it). 3. The attacker then issues, against their own WebDAV endpoint: `GET /..%2f/www/config.php`, `PUT /..%2f/www/shell.php`, `DELETE /..%2f/...`, etc. Impact: cross-tenant disclosure of source code, `.env`/DB credentials, private keys and backups; cross-tenant file write (webshell / defacement) and deletion. ## What I did and did not do (scope/ethics) - I demonstrated the **containment bypass** itself (the 404-vs-500 oracle above), reproduced on two independent servers, plus the version banner. - I did **not** read, write, or delete any file belonging to another customer. My three test accounts happened to be placed on three different backend servers, and I deliberately did not create the volume of accounts that forcing co-location would require, nor access any stranger's data. The cross-tenant read/write/delete consequence is drawn from the observed bypass plus the public CVE's own confirmed local proof (GET/PUT/DELETE outside root). - Test accounts: `mehdi2008kerimov+alwaysdata{1,2,3}@gmail.com` (`mehdik5100a`, `mehdik5100b`, `mehdik5100`). ## Severity CVSS 3.1 **8.7 / High** — `AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H` (remote; higher attack complexity because it needs same-server co-location and a prefix-shaped account name; attacker holds only their own low-priv free account; no user interaction; scope change into other tenants; full C/I/A over the victim account's files). Upstream rates the base CVE High. ## Remediation - Upgrade WsgiDAV to **>= 4.3.5** (4.3.4 fixes CVE-2026-48099; 4.3.5 also fixes the leaf-only `follow_symlinks=false` guard, CVE-2026-55560, which matters on shared hosting where home dirs contain symlinks). - Independently, confine each WebDAV worker to its account at the OS level (per-account `chroot` / mount namespace / bind-mount of only `/home/`) so that a library path-normalization bug cannot cross tenant boundaries. - Consider suppressing the WsgiDAV version string and Python exception text in error responses. — Mehdi Kerimov