Repository navigation
[fix] Reject unsafe navigation network paths - #17301
Conversation
Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
✅ PR preview is ready!
|
|
There was a problem hiding this comment.
Summary
This PR extracts st.Page's pre-resolution path check into _raise_if_unsafe_page_path and calls it from st.switch_page and st.page_link before os.path.realpath. The helper rejects null bytes on every platform and UNC, device-namespace, and extended-prefix paths on Windows, so an attacker-controlled page path cannot open an SMB connection or a Windows device before the target is confirmed to be a registered page.
All three expected reviews completed (claude-opus-5-5-medium, gpt-5.6-terra-xhigh, and grok-4.7-xhigh). They agree on the helper design, call-site ordering, unit-test strategy, compatibility boundary, and external-test recommendation. Two approve. gpt-5.6-terra-xhigh requests changes because a drive letter such as Z:\ can still reach realpath when that letter is mapped to a share. That case stays allowed under the public absolute-path contract; the assessment is in Security & Risk.
Product Alignment
The user-facing change is error behavior. st.switch_page and st.page_link now raise StreamlitAPIException for null-byte paths and, on Windows, for UNC and device prefixes. The existing st.Page message for that case changes from "Network paths are not supported." to "Network paths and device paths are not supported." The error_id values stay page-path-contains-null-bytes and page-network-path-not-supported.
This is a security bug fix. specs/AGENTS.md does not require a product spec for bug fixes. Specs that mention st.switch_page or st.page_link cover other features (parallel fragments, page run-every, layout) and do not define path validation. The PR body states the compatibility boundary and does not claim a separate product approval.
The implementation follows the contract st.Page already documents: a page path may be absolute or relative to the entrypoint. Relative paths, parent-relative paths, ordinary local absolute paths (including drive letters), Page objects, and external URLs keep working. st.page_link still classifies HTTP(S) URLs before the path check. Invalid input fails with a stable error_id before a filesystem side effect. That matches the API principles that apply here: the common case stays a relative or local path, the same page argument fails the same way across st.Page, st.switch_page, and st.page_link, and the change adds no new permanent surface area.
Code Quality
The change is small and proportionate. One private helper in lib/streamlit/navigation/page.py (lines 39–54) replaces the inline st.Page checks and is used by st.switch_page (lib/streamlit/commands/execution_control.py line 472) and st.page_link (lib/streamlit/elements/widgets/button.py line 1663). Both call sites already import from navigation.page, and importing a _-prefixed helper matches the existing _validate_registered_page pattern.
Call order is right. st.page_link returns external URLs first, then validates, then joins and calls realpath. st.switch_page converts a Path to str, validates, then calls normalize_path_join and realpath. Page objects still go through registration checks; their filesystem paths were already validated in Page.__init__. The helper reuses is_windows_unc_path, which treats any two leading separators as unsafe after normalizing / to \. That covers \\, //, /\, \/, \\?\, and \\.\.
Performance
No meaningful performance impact. Each path argument does a null-byte scan and, on Windows, a short prefix check. The work runs only for st.switch_page and st.page_link path inputs, performs no I/O, and sits outside the backend hot paths (element enqueue, ForwardMsg serialization, reruns, session state, caching, dataframes) and the frontend. Benchmark coverage is unnecessary.
Test Coverage
Unit coverage is sufficient, and skipping e2e is appropriate. The protected side effect is Windows filesystem resolution, which Linux Playwright runs would not exercise. All three reviews agree.
st.switch_pagerejects UNC, mixed-separator, extended, and device prefixes, including aPath, and assertsos.path.realpathandrequest_rerunare not called. Null bytes are rejected beforerealpathfor bothstrandPath.- The existing success test now runs with
IS_WINDOWSpatched on and still accepts relative paths, parent-relative paths,Pathobjects, and a drive-absolute path. st.page_linkhas the same rejection matrix, plus an allow-list for drive-absolute paths, parent-relative paths, and//server/share/...whenIS_WINDOWSis false. These tests run underDeltaGeneratorTestCase, which installs a script context, so they reach the validator.st.Pagegains the\\?\C:\...case, so the extended local prefix is covered on the original call site too.
claude-opus-5-5-medium suggested a matching non-Windows double-slash case on st.switch_page. The platform gate lives in the shared helper, and st.Page plus st.page_link already cover that allow path, so a third copy is optional.
Backwards Compatibility
Signatures, return types, configuration, and the navigation protocol are unchanged. Supported forms stay supported: paths relative to the entrypoint, parent-relative paths, ordinary local absolute paths (including drive letters), Page objects, external URLs, and non-Windows double-slash paths.
Two narrow behavior changes are intentional and match st.Page:
- On Windows,
st.switch_pageandst.page_linkraise for UNC, device-namespace, and extended-prefix paths. An app on a network share can still navigate with a path relative to the entrypoint. An absolute UNC argument, including one built from__file__when the process was started from a share, now raises. - Null bytes in those path arguments raise
StreamlitAPIExceptionon every platform. They previously failed later insiderealpath. st.Page's message for this case now says "Network paths and device paths are not supported." Theerror_idis unchanged.
Security & Risk
The patch closes the reported gap. On Windows, os.path.realpath can open an SMB connection and disclose NTLM credentials before the resolved path is compared with registered pages. Both production call sites now reject UNC and device prefixes first, and the tests pin that realpath is not called. Error text and error_id omit the path. There are no new dependencies, endpoints, HTML rendering, or dynamic code execution.
gpt-5.6-terra-xhigh flags drive-letter paths (Z:\...) as a remaining high-severity hole, because is_unsafe_path_pattern notes that realpath on a mapped drive can use SMB. claude-opus-5-5-medium and grok-4.7-xhigh treat drive letters as an intentional allow. The allow is the correct scope for this fix:
st.Page,st.switch_page, andst.page_linkdocument that a page path may be absolute. The PR body and the new tests keepC:\app\page.py. Drive-qualified absolute paths remain valid input, includingst.switch_page(Path(__file__))when the app lives on a mapped drive.- A UNC path embeds the remote host (
\\attacker\share). A drive letter contacts the host that letter is already mapped to on the machine. The path string does not choose the SMB server.is_unsafe_path_patternrejects drive letters because static and component file serving must stay inside a relative root. Navigation is a different API, and this PR correctly reusesis_windows_unc_path. - Separating mapped drives from local drives requires a Windows drive-type query. That is outside a lexical pre-check and would reject legitimate absolute paths on mapped drives.
Relative paths joined onto an entrypoint that itself lives on a UNC share can still touch that share. Apps hosted on a network share need that, and only the user-supplied argument is checked.
External test recommendation
- Recommend external_test: No
- Triggered categories: None
- Evidence:
lib/streamlit/navigation/page.py: lexical null-byte and Windows UNC/device check before filesystem access. No route, cookie, websocket, iframe, asset, or CORS changes.lib/streamlit/commands/execution_control.py:st.switch_pagecalls the helper beforeos.path.realpath.lib/streamlit/elements/widgets/button.py:st.page_linkcalls the helper only for non-URL paths, beforeos.path.realpath.
- Suggested external_test focus areas: None. Windows UNC rejection is covered by unit tests that patch
IS_WINDOWSand assertrealpathis not called. - Confidence: High
- Assumptions and gaps: External and embedded hosts differ for proxies, iframes, cookies, and asset URLs. This diff leaves those surfaces unchanged. External e2e hosts also will not reproduce Windows SMB resolution; the unit tests mock the platform check for that reason.
Accessibility
No frontend or rendered UI changes. Accessibility does not apply.
Readability
Non-blocking. The helper name and docstring are clear enough to use. Optional polish:
lib/tests/streamlit/navigation/page_test.py
test_rejects_windows_network_paths_before_resolving(line 174). The test now includes device-namespace and extended local prefixes. Proposed name:test_rejects_unsafe_windows_paths_before_resolving. Proposed docstring opening: "Windows network and device paths are rejected before any filesystem access."
lib/tests/streamlit/elements/page_link_test.py
test_allows_supported_local_paths(lines 114–117).//server/share/page.pywithIS_WINDOWSfalse is the non-Windows double-slash compatibility case. Proposed name:test_allows_paths_accepted_by_the_public_api. Proposed docstring:"""Relative, drive-absolute, and non-Windows double-slash paths still resolve."""
lib/tests/streamlit/commands/execution_control_test.py
test_st_switch_page_with_path_argument(line 614). The docstring still says the test only resolvesstrandPath. It now also passes a parent-relative path and a Windows drive path underIS_WINDOWS=True. Proposed docstring:"""``switch_page`` resolves string and ``Path`` arguments, including parent-relative and Windows drive paths."""
PR title and description
- Title:
[fix] Reject unsafe navigation network pathsis accurate and fits the usual prefix and length. One review preferred[fix] Reject unsafe Windows paths in navigation APIs. The current title is the better fit, because null-byte rejection applies on every platform. - Description: The summary states the user-visible effect. Worth adding the threat (an SMB connection that can disclose NTLM credentials) and the small
st.Pageerror-text change. Proposed opening:
Rejects attacker-controlled Windows network and device paths in `st.switch_page` and `st.page_link` before filesystem resolution, using the same checks as `st.Page`.
- Rejects null bytes on every platform. On Windows, also rejects UNC paths (shares such as `\\server\share`), device-namespace paths (`\\.\`), and extended prefixes (`\\?\`) so resolution cannot open an SMB connection.
- Still accepts relative paths, parent-relative paths, ordinary local absolute paths, `Page` objects, external URLs, and non-Windows double-slash paths.
- `st.Page`'s message for this case is now "Network paths and device paths are not supported." The `error_id` is unchanged.Recommendations
- Optional: extend the PR description with the SMB/NTLM motivation and the small
st.Pageerror-text change. - Optional: with an empty
error_prefix,st.switch_pageandst.page_linkraise "Network paths and device paths are not supported." without naming the command.st.Pagealready passes a prefix. A short hint such as "Use a path relative to the app entrypoint." would make the failure easier to act on. The current wording matches the existingst.Pagemessage, so this can wait.
Verdict
APPROVED: Two of three reviews approve, and the mapped-drive objection is the documented absolute-path contract. The shared pre-resolution check closes the Windows network-path gap in st.switch_page and st.page_link, keeps supported path forms working, and is covered by unit tests.
This is an automated AI review consolidated by grok-4.7-xhigh from claude-opus-5-5-medium, gpt-5.6-terra-xhigh, and grok-4.7-xhigh. All expected reviews completed. Please verify the feedback and use your judgment.
This review also includes 1 inline comment(s) on specific code lines.
Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
There was a problem hiding this comment.
Summary
All four expected reviews completed: gpt-5.6-sol-high, claude-opus-5-5-high, gemini-3.8-flash-high, and grok-4.7-xhigh. None failed. All four approve.
This PR stops st.switch_page and st.page_link from passing a Windows UNC or device path to os.path.realpath before Streamlit knows the path is a registered page. On Windows, that resolution can open an SMB connection and disclose the server process's NTLM credentials. The existing st.Page checks move into _raise_if_unsafe_page_path in lib/streamlit/navigation/page.py. The helper rejects null bytes on every platform and, on Windows, UNC, device-namespace, and extended-prefix paths. st.Page's message now says "Network paths and device paths are not supported." and keeps error ID page-network-path-not-supported.
Reviewers agreed the fix is small, ordered correctly, and covered by unit tests that assert filesystem resolution does not run. They differed on hosted external e2e coverage and on how complete the compatibility story is. Those are resolved below: no external test, and the Windows UNC/device rejection is an intentional, rare compatibility change that matches st.Page.
Product Alignment
This is a user-facing security bug fix. specs/AGENTS.md exempts bug fixes from a product spec. No merged navigation-path spec applies, and the PR body does not claim a separate product approval. The change adds no command, parameter, or return type.
All four reviewers agreed the behavior matches Streamlit's API principles. A relative page path still works. st.switch_page and st.page_link now use the path rules st.Page already enforced (consistency, and the same behavior for the same kind of path). Invalid paths raise StreamlitAPIException immediately, reusing page-network-path-not-supported and page-path-contains-null-bytes. The Windows-only network check leaves non-Windows // paths working.
Code Quality
The shared helper is the right size for this fix. Both new call sites convert Path to str, return early for Page objects, and st.page_link returns early for external URLs before the helper and before os.path.realpath. is_url accepts only http and https, so a UNC string is not treated as an external link. st.Page calls the helper before Path.resolve() and is_file().
is_windows_unc_path treats any two leading separators as unsafe after slash normalization, so \\, //, /\, \/, \\?\, and \\.\ are covered. normalize_path_join is os.path.normpath(os.path.join(...)). On Windows, a joined UNC path comes from an absolute UNC argument, which the helper rejects, or from a main script that already lives on a UNC share, which the operator controls. Checking the caller-supplied string is enough. Reviewers who traced that path agreed, and the implementations in lib/streamlit/file_util.py and lib/streamlit/path_security.py match that reading.
Performance
No meaningful performance impact. The new work is a null-byte scan and, on Windows, a slash-normalized prefix check on one navigation argument. That sits outside the backend hot paths (element enqueueing, ForwardMsg serialization, rerun state, caching, and dataframe/Arrow). Rejecting a UNC path also avoids a blocking SMB round trip. No benchmark is warranted. All four reviewers agreed.
Test Coverage
Unit coverage is sufficient, and e2e coverage is not needed. The switch_page, page_link, and st.Page tests reject the same Windows prefix matrix, including Path inputs and mixed separators, and assert that filesystem resolution does not run. Null-byte tests leave IS_WINDOWS unpatched. Allowed cases cover parent-relative paths, drive-absolute paths, and non-Windows // paths. The Linux e2e environment cannot exercise the Windows SMB side effect, so mocking realpath and Path.resolve is the right guard.
claude-opus-5-5-high suggested that test_allows_paths_accepted_by_the_public_api also assert page_script_hash. A missing match already raises StreamlitPageNotFoundError when the hash stays empty, so that test already fails if a path passes validation and then fails to match a registered page. An explicit hash assertion would only document the expected page. It is optional.
Backwards Compatibility
Supported navigation stays the same: relative and parent-relative paths, local absolute paths such as C:\app\page.py, Page objects, and external URLs. On non-Windows, //server/share/... remains a normal filesystem path. Error IDs are unchanged.
gemini-3.8-flash-high described the change as fully compatible and limited to paths that could never resolve to a registered page. claude-opus-5-5-high, gpt-5.6-sol-high, and grok-4.7-xhigh identified a real restriction, and the code matches that reading:
- On Windows,
st.switch_pageandst.page_linknow raise for UNC and device prefixes. An absolute UNC path, includingPath(__file__)when the app itself is on a network share, can no longer be passed even when that file is a registered page. A relative path from that same app still works, because the helper inspects the argument and leaves the joined result alone. - Null-byte paths raise
StreamlitAPIExceptionon every platform. Previously they reachedos.path.realpathand surfaced as a rawValueError. st.Page's sentence changed from "Network paths are not supported." to "Network paths and device paths are not supported." Callers matching the old sentence would see a difference.
That restriction is the security fix, it matches the existing st.Page contract, and it is acceptable to ship.
Security & Risk
The PR closes the credential-disclosure path. st.switch_page and st.page_link previously called os.path.realpath on the caller-supplied path before checking that it was a registered page. On Windows, that resolution can open an SMB connection for a UNC path. The helper runs first and does not touch the filesystem. path_security.py also describes this class of check as blocking SSRF and NTLM disclosure; the concrete effect here is the SMB connection.
Accepted residual risk, consistent with st.Page and with the public navigation contract:
- A drive letter mapped to a share, such as
Z:\, stays allowed. Resolution can still touch that share. The operator configured the mapping. - A relative path that traverses an existing symlink or junction to a UNC target is invisible to a lexical prefix check.
AppTest.switch_pagestill callsPath.is_file()andPath.resolve()on a path joined to the test app. That helper is a local test harness, and its docstring requires a path relative to the main script. It is outside this change.
No new dependency, HTML rendering, subprocess, auth, or session change.
External test recommendation
- Recommend external_test: No
- Triggered categories: None
- Evidence:
lib/streamlit/navigation/page.py: lexical null-byte and Windows UNC/device check before filesystem access. No Starlette route,baseUrlPath, redirect, status code, cookie, WebSocket, iframe, asset, CORS, storage, or CSP change.lib/streamlit/commands/execution_control.py:st.switch_pagecalls the helper beforeos.path.realpath. Script-path resolution only.lib/streamlit/elements/widgets/button.py:st.page_linkcalls the helper for non-URL filesystem paths, after the external-URL return.is_urlstill requires anhttporhttpsscheme, so UNC strings are not treated as external links.
- Suggested external_test focus areas:
- None. Hosted external e2e runs on Linux and would not execute the
IS_WINDOWSrejection. Valid relative-path navigation is unchanged.
- None. Hosted external e2e runs on Linux and would not execute the
- Confidence: High
- Assumptions and gaps: Assumes
@pytest.mark.external_testtargets proxied, embedded, and SiS/Snowflake browser deployments on Linux. The Windows prefix matrix is covered by unit tests withIS_WINDOWSpatched.
gpt-5.6-sol-high recommended Yes under "Routing and URL behavior" and "Cross-origin behavior and external networking", with a hosted relative-path smoke test. The other three recommended No. The checklist applies to Starlette routing, URL status codes, CORS, crossOrigin, and browser-side external fetches. This diff leaves those alone. Stopping a Windows SMB connect from os.path.realpath is server-side filesystem policy, and the external suite cannot observe it. A relative-path smoke test would re-check behavior this PR leaves in place.
Accessibility
No frontend or rendered-UI changes. Not applicable. All four reviewers agreed.
Readability
PR title. [fix] Reject unsafe navigation network paths matches [type] Description and is within 63 characters. gemini-3.8-flash-high, gpt-5.6-sol-high, and grok-4.7-xhigh accepted it. claude-opus-5-5-high found "navigation network paths" easy to misread and proposed [fix] Reject Windows network paths in switch_page and page_link. The current title stands alone. The alternative names the commands more clearly if the changelog entry should do that.
PR description. The opening sentence states the change and the credential-leak reason. The template sections are present. gpt-5.6-sol-high flagged the trailing "Made with Cursor" line as meta-commentary. Remove that line. claude-opus-5-5-high suggested one sentence on reachability: the SMB path matters when an app passes untrusted input, such as a query parameter, to these commands. That sentence helps a reviewer gauge severity.
Code. gpt-5.6-sol-high found the helper name, security comment, and test names clear. The other reviewers asked for optional docstring and comment polish. Specific wording follow-ups are in the inline comments. The helper comment already states that resolving these prefixes can open an SMB connection and why a drive-absolute path stays allowed.
Recommendations
- Optional: remove the "Made with Cursor" line from the PR description, and add one sentence that this is reachable when an app passes untrusted input to
st.switch_pageorst.page_link. - Optional: if the changelog title should name the commands, use
[fix] Reject Windows network paths in switch_page and page_link.
Verdict
APPROVED: All four reviews approve this focused security fix, the unit tests cover the Windows path rejection, and the remaining notes are optional wording follow-ups.
This is an automated consolidated AI review by grok-4.7-xhigh. Please verify the feedback and use your judgment.
This review also includes 2 inline comment(s) on specific code lines.
There was a problem hiding this comment.
Auto-approved by AI final review (workflow run).
Co-authored-by: lawilby <laura.wilby+oss@snowflake.com>
Describe your changes
Prevents
st.switch_pageandst.page_linkfrom resolving attacker-controlled Windows network or device paths before confirming that the target is a registered page, avoiding filesystem resolution that could initiate an SMB connection and disclose NTLM credentials. This is reachable when an app passes untrusted input, such as a query parameter, to either command.st.Page's pre-resolution validation across navigation APIs, rejecting null bytes on all platforms and UNC/device prefixes on Windows.Pageobject, and external URL navigation.st.Page's error text to mention device paths while preserving its existing error ID.GitHub Issue Link (if applicable)
N/A — defense-in-depth security finding.
Testing Plan
switch_page, andpage_linkpath rejection and compatibility coverageContribution License Agreement
By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.
Note
Low Risk
Defense-in-depth security tightening with shared validation and broad unit tests; behavior change is limited to rejecting previously dangerous path forms before resolve.
Overview
Hardens navigation APIs against unsafe page paths when apps pass untrusted strings (e.g. query params) into
st.switch_pageorst.page_link.Extracts
_raise_if_unsafe_page_pathfrom the existingst.Pagechecks and runs it before any path resolution (os.path.realpath/Path.resolve). It rejects null bytes everywhere and, on Windows, lexical UNC, extended-prefix, and device-namespace paths that could trigger SMB or direct device access and leak NTLM credentials.st.Pagenow uses the same helper with slightly clearer errors (device paths + guidance for local absolute paths).Supported navigation is unchanged: relative and parent-relative paths, drive-absolute paths like
C:\app\page.py,Pageobjects, external URLs, and non-Windows//spellings. Unit tests assert unsafe paths fail without calling filesystem helpers, and expand allowed-path coverage forswitch_pageandpage_link.Reviewed by Cursor Bugbot for commit 8469b95. Bugbot is set up for automated code reviews on this repo. Configure here.