Skip to content

Jupyter, VM2, and token-type boundary batch

Source: GitHub Security Advisories, published/updated 2026-05-05.

This batch is durable because all three clusters are recurring trust-boundary mistakes: URL/path prefix checks that are not canonical boundary checks, JavaScript sandbox escapes that rely on new language/runtime surfaces, and JWT validation that treats any trusted signature as sufficient proof of token purpose.

Advisories covered

  • Jupyter Server <= 2.17.0 — fixed in 2.18.0:
  • GHSA-5789-5fc7-67v3: authenticated path traversal via startswith() root-directory checks that can expose sibling directories sharing the same prefix.
  • GHSA-24qx-w28j-9m6p: CORS origin bypass when allow_origin_pat is evaluated with re.match() instead of a full-string match.
  • GHSA-5mrq-x3x5-8v8f: password reset and server restart do not revoke existing authentication cookies because the cookie secret remains persistent.
  • GHSA-qh7q-6qm3-653w: open redirect through insufficiently validated next query parameter.
  • vm2 sandbox escapes:
  • GHSA-v37h-5mfm-c47c: inspect-function proxy unwrap breakout, fixed in 3.11.0 for <= 3.10.3.
  • GHSA-qvjj-29qf-hp7p: Promise species bypass of a prior sandbox fix, fixed in 3.10.5 for <= 3.10.3.
  • GHSA-55hx-c926-fr95: SuppressedError escape on modern Node, fixed in 3.11.0 for <= 3.10.4.
  • GHSA-ffh4-j6h5-pg66: WASM exception-handling / JSTag escape on Node 25, fixed in 3.10.5 for 3.10.4.
  • nuts-node <= 1.1.0 — GHSA-9hmg-827w-9rhj: v1 access-token introspection accepts Verifiable Presentation JWTs as active access tokens because it validates a known signature without enforcing JWT type, issuer-to-key binding, and required access-token claims. No patched version was listed in the advisory at scan time.

Operator triage

Jupyter Server

  1. Inventory exposed notebook/server instances and confirm jupyter-server version. Treat public, shared, or multi-tenant servers as priority targets.
  2. Upgrade to 2.18.0+.
  3. After upgrade, rotate secrets by deleting/regenerating the Jupyter cookie secret on affected hosts and force active sessions to reauthenticate. A password reset alone was not enough for the cookie-secret issue.
  4. Review configuration for allow_origin_pat; anchor patterns or convert them to exact-origin allowlists. Do not rely on prefix-like regex behavior.
  5. Hunt for path traversal attempts against /api/contents/ using encoded .., sibling directory names that share the configured root prefix, or checkpoint/create/delete calls outside expected notebooks.
  6. Hunt for login URLs with external next targets such as protocol-relative URLs (///host) or lookalike domains.

vm2

  1. Treat any service that runs attacker-controlled JavaScript in vm2 as high risk, especially if it is reachable from tenants, users, CI jobs, templates, extensions, agents, or plugin systems.
  2. Upgrade to a version containing the relevant fix; for mixed advisory coverage, prefer 3.11.0+ rather than stopping at a narrower 3.10.5 fix.
  3. If untrusted code has run, assume possible host-process command execution. Preserve logs, rotate secrets reachable by the Node process, and rebuild worker hosts from clean images when exposure was internet-facing or multi-tenant.
  4. Reduce blast radius even after patching: run sandbox workers in containers or microVMs, with no long-lived credentials, read-only filesystems where possible, egress allowlists, low privileges, and per-job teardown.
  5. Add regression tests for new JavaScript/runtime surfaces: SuppressedError, DisposableStack, Promise species manipulation, inspect/custom inspection, WASM exception handling, and current Node release lines.

nuts-node / JWT type confusion

  1. Do not treat a trusted JWT signature as sufficient authorization. Verify token family/type, issuer-to-key binding, audience, subject semantics, expiry, and all purpose-specific claims before returning active: true.
  2. For affected deployments, disable or restrict the v1 introspection endpoint until fixed, especially across trust domains where Verifiable Presentation JWTs may be attacker-controlled.
  3. Hunt for introspection calls where the presented JWT resembles a Verifiable Presentation rather than an access token, including missing service/purpose claims or ambiguous typ: JWT headers.
  4. Prefer explicit JOSE media types such as vp+jwt / at+jwt and reject ambiguous tokens at the boundary.

Durable controls

  • Replace string-prefix path containment with canonical path resolution plus boundary checks (commonpath/realpath-style logic), then test sibling-prefix cases (root vs root2).
  • Prefer exact origin allowlists or full-string regex matching for browser security decisions. Prefix matches are not origin checks.
  • Couple credential rotation with session-secret rotation. If the signing key remains valid, old cookies remain credentials.
  • Do not depend on language-level JavaScript membranes as the only isolation boundary for hostile code. Runtime features evolve faster than proxy-based sandboxes.
  • Model token validation as a typed protocol decision, not a crypto-only decision: every token class needs an allowlisted issuer, audience, type, and claim contract.

Jupyter CORS origin-regex validation update

Updated: hourly offensive-security scan, 2026-06-15. GitHub updated GHSA-24qx-w28j-9m6p / CVE-2026-40110 with a clear exploit precondition: Jupyter Server validates allow_origin_pat with Python re.match(), so an unanchored trusted-origin regex can match an attacker-controlled suffix origin. Upstream references include the Jupyter Server advisory, pull request 603, and patch commits 057869a and 49b3439.

Why this is operator-useful

This is a reusable browser-origin trust-boundary pattern: a server accepts a regex for trusted CORS origins, but evaluates it as a prefix match instead of a whole-origin match. A suffix domain such as https://trusted.example.com.attacker-owned.test can satisfy a pattern intended only for https://trusted.example.com when that pattern lacks explicit anchors.

Safe validation workflow

  1. Confirm the path exists. Test only deployments using allow_origin_pat or an equivalent regex-based CORS allowlist. Do not assume every Jupyter deployment is affected.
  2. Use an owned suffix origin. Host the proof page on a canary domain controlled by the test team, for example https://trusted.example.com.attacker-owned.test.
  3. Baseline regex behavior locally. Show the semantic mismatch without touching the target:
import re

pattern = r"https://trusted\.example\.com"
origins = [
    "https://trusted.example.com",
    "https://trusted.example.com.attacker-owned.test",
    "https://other.example.com",
]

for origin in origins:
    print(origin, bool(re.match(pattern, origin)), bool(re.fullmatch(pattern, origin)))
  1. Exercise only a harmless endpoint. From the canary origin, request an owner-approved status/version route with browser fetch(..., { credentials: "include", mode: "cors" }). The proof is the browser exposing a response or CORS header to the suffix origin, not access to notebooks or files.
  2. Add controls. Compare the suffix origin with a clearly unrelated origin and, where possible, a lab configuration using anchored ^...$ matching.

Evidence boundaries

  • Capture version/package evidence, the redacted allow_origin_pat shape, the request Origin, and the CORS response header.
  • Keep screenshots and HAR files limited to harmless status/version responses.
  • Never collect notebooks, terminals, tokens, model files, datasets, environment variables, cloud credentials, or user content.
  • Report the root cause as prefix regex matching used for origin authorization, not merely generic CORS misconfiguration.

Jupyter Server sibling-prefix path traversal follow-up

Updated: hourly offensive-security scan, 2026-07-09. GitHub updated GHSA-gf7q-q4j7-hp7c / CVE-2026-5422 with a clearer root cause and patched version boundary: jupyter-server before 2.18.2 can allow authenticated file access outside root_dir when _get_os_path() checks path.startswith(root) without a trailing path separator and to_os_path() preserves traversal parts. The operator-useful pattern is the same durable class as the original Jupyter entry: string-prefix containment is not filesystem containment.

Safe validation workflow

  1. Build a disposable lab tree. Use a throwaway user and temporary directory names that intentionally share a prefix, for example /tmp/jupyter-root and /tmp/jupyter-root-sibling. Put only a synthetic marker file in the sibling directory.
  2. Confirm the target shape. Validate only Jupyter Server deployments where the assessment scope includes authenticated Contents API testing and the package version is below 2.18.2.
  3. Probe with canary paths only. Exercise /api/contents/ with encoded traversal and sibling-prefix path variants that should remain outside root_dir. The proof is access to the synthetic sibling marker or an unexpected write to a disposable sibling path.
  4. Capture negative controls. Show that a non-prefix sibling such as /tmp/other-sibling is denied, and that the same canary path is denied after upgrading or applying a canonical boundary check.
  5. Stop at boundary proof. Do not enumerate directories or read real notebooks, datasets, credentials, terminals, model artifacts, or cloud configuration files.

Evidence to keep

  • jupyter-server version, configured root_dir shape, and the redacted Contents API route used.
  • The synthetic sibling directory names and marker filename, with contents limited to a harmless canary string.
  • A before/after result table covering vulnerable, patched, prefix-sibling, and non-prefix-sibling cases.
  • Source links: GitHub advisory, huntr report, and upstream fix commit.