## Ticket: CVE-2026-35397 — Jupyter Server path traversal via faulty `startswith()` containment check

**Advisory**: GHSA-5789-5fc7-67v3 — https://github.com/jupyter-server/jupyter_server/security/advisories/GHSA-5789-5fc7-67v3
**CVE**: CVE-2026-35397 | **CWE-22** (Path Traversal)
**Severity**: High — CVSS 7.6
**Package**: `jupyter-server` (PyPI) | **Repository**: https://github.com/jupyter-server/jupyter_server

### Impact

Jupyter Server confines file access to a configured **root directory**. To
enforce this, it resolves the requested path and checks that it is contained
within the root by performing a plain string `startswith()` test against the
configured root path.

`startswith()` matches *string prefixes*, not *path-component boundaries*. A
sibling directory whose name merely shares a prefix with the root therefore
passes the check. If the root is `/srv/data`, then `/srv/data-secret` also
"starts with" `/srv/data`, so a file under `/srv/data-secret` is incorrectly
treated as living inside the root.

An attacker who can reach the server's file/contents endpoints can use this to
read files outside the configured root directory — any directory whose path
string begins with the root path.

### Affected / fixed versions

Affected: `jupyter-server` `<= 2.17.0`.
Fixed: **`2.18.0`**.

Reproduce on a vulnerable build (**`2.17.0`**) and verify the fix on
**`2.18.0`**.

### Where to look

The repository is https://github.com/jupyter-server/jupyter_server . Inspect
the diff between the tags to confirm the root cause and the guard:

```bash
git clone https://github.com/jupyter-server/jupyter_server.git
cd jupyter_server && git diff v2.17.0 v2.18.0
```

The fix replaces the string-prefix `startswith()` containment check with a
proper path-component-aware comparison so that prefix-sharing siblings are no
longer treated as inside the root directory.

### Reproduction approach

No database or browser is required — the difference is observable in the HTTP
response.

1. In a scratch directory, install the **vulnerable** build:
   `pip install jupyter-server==2.17.0`
2. Create a root directory (e.g. `/tmp/srv/data`) and a prefix-sharing sibling
   directory (e.g. `/tmp/srv/data-secret`), placing a target file with known
   contents inside the sibling directory.
3. Start the server with that root directory and request the sibling file via
   the file/contents endpoint.
4. Repeat with the **fixed** build: `pip install jupyter-server==2.18.0`.

### Expected result

| Build | Request for prefix-sharing sibling file | Observable |
|-------|------------------------------------------|------------|
| `jupyter-server==2.17.0` (vulnerable) | passes the `startswith()` check | **HTTP 200** with the sibling file's contents |
| `jupyter-server==2.18.0` (fixed) | rejected by the path-boundary check | **HTTP 403/404**, contents not returned |

- **Vulnerable indicator**: the server serves a file from the prefix-sharing
  sibling directory (HTTP 200 with file contents).
- **Fixed indicator**: the server refuses the request (HTTP 403/404) and the
  file outside the root is not disclosed.

### Expected artifacts

- `reproduction_steps.sh` — installs both versions, sets up the root and
  prefix-sharing sibling directories, starts the server, and requests the
  sibling file for each build.
- `validation_verdict.json` — `verdict: confirmed`, with the vulnerable and
  fixed indicators captured.
- Logs capturing the HTTP responses for `2.17.0` and `2.18.0`.
