Skip to content
Renesoft
About Products Services Robotics Cybersecurity Careers Contact
EN SV

Security Advisories

CVE-2026-108258

Shiny for Python — Path traversal in bookmark restore

Medium 6.9 CWE-22 Python · PyPI
6.9/10MediumCVSS 4.0 base score
LowMediumHighCritical
04.07.09.010
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N

Summary

Shiny for Python's bookmark-restore path accepts a client-supplied _state_id_ query value and joins it into the server-side bookmark directory without validating it. Parent-directory segments or an absolute path make the server open input.json and values.json from outside the bookmark store.

Technical analysis

The restore handler builds <cwd>/shiny_bookmarks/<id> from the _state_id_ query-string value with no check that the id is a single safe path segment, so .. segments or an absolute path escape the store. On a default application the restore is attempted whenever the client supplies a query string, including with bookmark_store="disable", so no opt-in configuration is required. Applications configured with bookmark_store="server" and using ui.input_file() are affected more seriously: the file-input restore handler copies files out of the restore directory, so the contents of an attacker-selected file in an attacker-selected directory can be read into the application.

The sink joins the id with no check. _local_dir(id) in shiny/bookmark/_bookmark_state.py returns Path(os.getcwd()) / "shiny_bookmarks" / id with no containment check, so an id of ../.. walks straight out of the bookmark store.

The id is request-controlled. RestoreContext._load_state_qs takes _state_id_ from the query string and passes it to the restore-dir function unchanged. On websocket init the session hands the browser's URL query string (.clientdata_url_search) straight to RestoreContext.from_query_string, so the attacker's _state_id_ reaches the sink verbatim. shiny/bookmark/_restore_state.py

What the restore reads. On restore the server opens input.json and values.json under the resolved directory and JSON-parses them into the restored state. A traversal id therefore reads those two filenames from a directory outside the store — a read-only effect (confidentiality), with no write and no code execution. CVSS 4.0 scores it VC:L, 6.9 Medium.

shiny/bookmark/_bookmark_state.py (1.6.3) — the client id is joined onto the store path with no validation
shiny_bookmarks_folder_name = "shiny_bookmarks"


def _local_dir(id: str) -> Path:
    # Try to save/load from current working directory as we do not know
    # where the app file is located
    return Path(os.getcwd()) / shiny_bookmarks_folder_name / id


async def local_restore_dir(id: str) -> Path:
    return _local_dir(id)

Proof of concept

Renesoft confirmed this against the real package in an isolated environment, with no live systems. The attack path, then the recorded test output:

  1. The target runs with server-side bookmarking (bookmark_store="server"). Saved states live under ./shiny_bookmarks/<id>/ relative to the process working directory, each holding an input.json and an optional values.json.
  2. On connection the client sends its URL query string as .clientdata_url_search. If it contains _state_id_, RestoreContext._load_state_qs loads that state from disk.
  3. The id is used verbatim: Path(os.getcwd()) / "shiny_bookmarks" / id. A request with _state_id_=../<elsewhere> resolves to os.getcwd()/<elsewhere>, outside the store.
  4. If that directory holds an input.json (and/or values.json), the server reads and JSON-parses them into the restored state — reading those files from a location the attacker chose.
  5. The effect is confidentiality, limited to the two bookmark filenames reachable by traversal from the working directory.
poc — driven through the real RestoreContext.from_query_string in an isolated temp sandbox; the out-of-bounds target is a sibling of the bookmark store inside that same sandbox, never a real system path. Vulnerable 1.6.3, then fixed 1.6.4 with the same payload.
# shiny 1.6.3 — VULNERABLE
[control] _state_id_ = 'legit-0001'              (normal, in-bounds)
  resolved dir   : <sandbox>/shiny_bookmarks/legit-0001
  restored input : {'txt': 'hello from a legit bookmark'}

[attack]  _state_id_ = '../OUT_OF_BOUNDS_secret'   (path traversal)
  resolved dir   : <sandbox>/OUT_OF_BOUNDS_secret
  escaped store? : True
  restored input : {'txt': 'SECRET-MARKER-9f2a: this file is OUTSIDE shiny_bookmarks'}
  RESULT         : TRAVERSAL SUCCEEDED - read input.json OUTSIDE the store

# shiny 1.6.4 — FIXED (same payload)
[attack]  _state_id_ = '../OUT_OF_BOUNDS_secret'
  Could not restore bookmarked state: Invalid bookmark id: '../OUT_OF_BOUNDS_secret'
  active         : False
  resolved dir   : None
  escaped store? : False
  RESULT         : traversal did NOT leak out-of-bounds content

Impact

On a default application the result is an unauthenticated, server-side probe for the existence and JSON-validity of attacker-chosen paths, constrained to the two file names input.json and values.json. With server-side bookmark storage and a file input, an attacker-chosen file's contents can be read into the application.

Affected versions

Affected
1.4.0 – 1.6.3
Fixed
1.6.4
CVSS
6.9 (4.0)
Weakness
CWE-22
Ecosystem
Python · PyPI
Disclosed
2026-09-03

Remediation

Fixed in 1.6.4 (pip install --upgrade shiny). The fix validates bookmark ids against an allowlist of a single path segment matching [A-Za-z0-9_-]+, which cannot express a separator, a .. parent reference, or an absolute path, and gates restore on the bookmark_store setting at a single point. Bookmark ids generated by Shiny are unaffected by the restriction.

Timeline

  • 2026-09-03Published by the maintainer with the fix in 1.6.4; GHSA-47c3-hpmg-7j6p.
  • 2026-10-09CVE-2026-108258 record published at cve.org.

References

  • GHSA-47c3-hpmg-7j6p
  • CVE-2026-108258
  • Fix commit 1d8ecb4
  • Release 1.6.4
  • PyPI: shiny
  • Source

← All advisories

Renesoft

Conviction, compiled.

© 2026 Renesoft ABReg. no. 559593-4489Malmö, SwedenCareersCybersecurityPrivacy policy
v7.4.2 · cbb0edd