Skip to content

[security] Prevent SSRF attacks via path traversal in component file handling#13733

Merged
sfc-gh-nbellante merged 14 commits intodevelopfrom
nbellante/fix-unc-path-ssrf-vulnerability
Feb 3, 2026
Merged

[security] Prevent SSRF attacks via path traversal in component file handling#13733
sfc-gh-nbellante merged 14 commits intodevelopfrom
nbellante/fix-unc-path-ssrf-vulnerability

Conversation

@sfc-gh-nbellante
Copy link
Copy Markdown
Contributor

@sfc-gh-nbellante sfc-gh-nbellante commented Jan 28, 2026

Describe your changes

Added centralized path security validation to prevent Server-Side Request Forgery (SSRF) and path traversal vulnerabilities. The changes include:

  1. Created a new path_security.py module with is_unsafe_path_pattern() function that detects potentially dangerous paths:

    • UNC paths (Windows network shares)
    • Absolute paths (Windows drive letters or root-based paths)
    • Path traversal attempts using ..
    • Null byte injection attempts
    • Windows special path prefixes
  2. Added a new PathSecurityMiddleware for Starlette that blocks unsafe paths at the HTTP layer before any filesystem operations.

  3. Updated component path handling to validate paths BEFORE calling os.path.realpath(), preventing Windows from triggering SMB connections to attacker-controlled servers.

  4. Modified AppStaticFileHandler to check for unsafe paths before filesystem operations.

  5. Standardized security responses across the codebase (400 Bad Request for malicious paths).

Testing Plan

Added comprehensive test cases covering:

  • Detection of various unsafe path patterns
  • Validation of UNC paths and Windows drive letters
  • Handling of URL-decoded paths
  • Null byte injection attempts
  • Windows special path prefixes
  • Mixed path separator handling
  • Verification that legitimate paths still work correctly
  • Middleware positioning and integration with the request pipeline
  • Proper handling of WebSocket connections

The tests verify both the new security functions and their integration across different parts of the codebase.


Contribution License Agreement

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.

Copilot AI review requested due to automatic review settings January 28, 2026 17:58
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Jan 28, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 28, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-13733/streamlit-1.53.1-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-13733.streamlit.app (☁️ Deploy here if not accessible)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Addresses an SSRF risk on Windows by adding early-path validation to block UNC/absolute/traversal inputs before any filesystem resolution occurs.

Changes:

  • Added _is_unsafe_path and used it in build_safe_abspath to reject UNC/absolute/traversal paths before calling os.path.realpath.
  • Expanded unit tests to cover UNC paths, Windows drive paths, traversal patterns, null bytes, and mixed separators.
  • Updated existing normalization-related tests to reflect the stricter traversal policy.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
lib/streamlit/web/server/component_file_utils.py Adds early path validation to prevent UNC-triggered network access and harden path resolution.
lib/tests/streamlit/web/server/component_file_utils_test.py Adds/updates unit tests to validate the new SSRF/traversal defenses and guard against regressions.

@sfc-gh-nbellante sfc-gh-nbellante changed the title Fix UNC path SSRF vulnerability (SNOW-3028489) [security] Prevent SSRF attacks via path traversal in component file handling Jan 28, 2026
@sfc-gh-nbellante sfc-gh-nbellante added the ai-review If applied to PR or issue will run AI review workflow label Jan 28, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 28, 2026

📈 Frontend coverage change detected

The frontend unit test (vitest) coverage has increased by 0.0500%

  • Current PR: 86.4300% (13706 lines, 1859 missed)
  • Latest develop: 86.3800% (13706 lines, 1866 missed)

🎉 Great job on improving test coverage!

📊 View detailed coverage comparison

@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Jan 28, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR adds security enhancements to prevent Server-Side Request Forgery (SSRF) vulnerabilities in component file handling. The key changes are:

  1. New _is_unsafe_path() function - Detects potentially dangerous path patterns (UNC paths, absolute paths, path traversal, null bytes) before any filesystem operations occur.
  2. Early validation in build_safe_abspath() - Validates paths BEFORE calling os.path.realpath() to prevent Windows from triggering SMB connections to attacker-controlled servers.
  3. Stricter path handling - Rejects paths containing .. segments even if they would normalize safely (defense-in-depth).

Code Quality

The implementation is well-structured and follows Streamlit's coding standards:

Strengths:

  • The _is_unsafe_path() function (lines 33-74 in component_file_utils.py) is properly prefixed with _ indicating it's private/internal use only
  • Comprehensive NumPy-style docstrings explaining the security rationale and parameters
  • Clear inline comments explaining each security check
  • The security comment in build_safe_abspath() (lines 97-99) clearly documents why early validation is necessary

Code structure in _is_unsafe_path():

# Check order is logical and well-commented:
# 1. Null bytes (path truncation attacks)
# 2. UNC paths (Windows network shares)
# 3. Windows drive letters (case-insensitive)
# 4. Rooted absolute paths
# 5. Path traversal segments

The path traversal detection correctly handles:

  • Mixed separators (\ and /)
  • Legitimate filenames containing dots (e.g., file..name.js, ..config.js)

Test Coverage

Excellent test coverage with 211 new lines of tests. The tests follow best practices from the Python test guide:

  1. Parameterized tests - Uses @pytest.mark.parametrize extensively with descriptive IDs
  2. Docstrings - All test functions have clear docstrings explaining purpose
  3. Anti-regression tests - Includes tests ensuring safe paths still work (test_safe_path_still_resolves_correctly, test_safe_nested_path_resolves)
  4. Negative assertions - Tests both that unsafe paths are rejected AND that safe paths are allowed

Test categories covered:

  • UNC paths (backslash and forward slash variants)
  • Windows drive letters (uppercase and lowercase)
  • Rooted absolute paths
  • Path traversal (simple, complex, mixed separators)
  • Null byte injection
  • URL-decoded malicious paths
  • Windows special path prefixes (\\?\, \\.\)
  • Edge cases (empty string, current directory, filenames with dots)

Minor observation: The import from urllib.parse import unquote inside test_url_decoded_paths_are_rejected could be moved to the top of the file for consistency, but this is a non-blocking style preference.

Backwards Compatibility

Breaking change: Paths containing .. segments that would have previously normalized safely (e.g., sub/../inside.txt) are now rejected. This change is:

  • Intentional and documented - The removed test case normalized_parent_segments is replaced with test_normalized_parent_segments_rejected() explaining this security-motivated behavior change
  • Low impact - Normal component file requests don't use .. in paths; this primarily affects attack payloads
  • Justified - Defense-in-depth against SSRF attacks where path traversal could be combined with other techniques

The change is unlikely to affect legitimate users since component URLs are typically clean paths like component_name/file.js.

Security & Risk

Security assessment: GOOD

The fix correctly addresses the SSRF vulnerability:

  1. Root cause addressed - The check runs BEFORE os.path.realpath(), preventing Windows from initiating SMB connections to attacker-controlled servers
  2. Comprehensive pattern detection - Covers UNC paths, drive letters, absolute paths, traversal, and null bytes
  3. Platform-agnostic - Rejects Windows-specific attacks even on Linux servers, which is the correct approach since:
    • Some deployments may use Windows
    • Defense-in-depth is appropriate for security code
  4. No bypass vectors identified - The implementation handles:
    • URL encoding (handled by framework before reaching this code)
    • Mixed path separators
    • Case variations in drive letters

Minor edge case not covered but acceptable: C:foo (Windows relative path on a specific drive) is not explicitly rejected, but this format is rarely used and would likely be caught by other security mechanisms downstream.

Recommendations

No blocking issues found. The following are optional improvements:

  1. Consider logging rejected paths (non-blocking): For security monitoring, it might be useful to log when unsafe paths are rejected. However, this should be done carefully to avoid log injection.

  2. Move import to top of file (style, non-blocking): In test_url_decoded_paths_are_rejected, the from urllib.parse import unquote import could be moved to the file's import section for consistency.

Verdict

APPROVED: This is a well-implemented security fix with comprehensive test coverage. The code follows Streamlit's conventions, the breaking change is justified and documented, and no security bypasses or regressions were identified. The PR is ready for merge.


This is an automated AI review using opus-4.5-thinking. Please verify the feedback and use your judgment.

@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nbellante/fix-unc-path-ssrf-vulnerability branch from 7e0c6c8 to ff25af4 Compare January 28, 2026 18:45
@sfc-gh-nbellante sfc-gh-nbellante added area:security Related to security concerns change:bugfix PR contains bug fix implementation impact:users PR changes affect end users labels Jan 28, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR centralizes path safety validation into a shared helper and applies it before any filesystem resolution in component and app static file handling, preventing UNC/drive-based SSRF vectors and tightening traversal checks. It also adds comprehensive Python unit tests for unsafe patterns and regression coverage around early rejection.

Code Quality

The refactor is clean and keeps path security checks in one place (lib/streamlit/path_security.py) while reusing it in both component routing and registration flows. Docstrings explain the security rationale and the call sites are updated consistently; I did not spot structural issues or regressions in the logic.

Test Coverage

Unit tests were added and are thorough, including regression checks that os.path.realpath is not called on unsafe inputs and coverage for UNC/drive/null-byte/traversal patterns (lib/tests/streamlit/web/server/component_file_utils_test.py, lib/tests/streamlit/path_security_test.py). No e2e or frontend tests were touched, which is appropriate for this backend-only change. Tests were not run locally (per instructions).

Backwards Compatibility

Behavior is intentionally stricter: paths containing .. segments and drive-qualified prefixes are now rejected even if they would normalize inside the root. This could affect components that rely on such paths, but the change is security-motivated and consistent across handlers.

Security & Risk

Security posture is improved: unsafe path patterns are rejected before any realpath calls, reducing SSRF/NTLM exposure on Windows. I did not find new security risks or regression hazards in the updated logic.

Recommendations

  1. Consider a brief release note or changelog entry describing the stricter path validation (rejection of .. segments and drive-qualified paths) to help users understand potential edge-case behavior changes.

Verdict

APPROVED: Security fix is well-scoped, tests are comprehensive, and no blocking issues were found.


This is an automated AI review using gpt-5.2-codex-high. Please verify the feedback and use your judgment.

@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nbellante/fix-unc-path-ssrf-vulnerability branch from f586843 to e7d904e Compare January 29, 2026 00:23
@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nbellante/fix-unc-path-ssrf-vulnerability branch from 5c81c2d to 0c2712e Compare January 29, 2026 17:52
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR hardens Streamlit’s static/component file-path handling to prevent Windows UNC-path SSRF/NTLM leakage by introducing a shared is_unsafe_path_pattern validator and ensuring validation happens before any os.path.realpath() calls. It also extends similar protection to the Tornado AppStaticFileHandler and adds comprehensive Python unit tests for the new behavior.

Code Quality

  • Good consolidation and consistency: Centralizing the logic in lib/streamlit/path_security.py reduces the risk of divergence between code paths and makes the security invariant (“validate before filesystem resolution”) explicit.
  • Good “early check” placement:
    • build_safe_abspath rejects unsafe patterns before any realpath() on untrusted input (lib/streamlit/web/server/component_file_utils.py:57-73).
    • AppStaticFileHandler.get_absolute_path rejects unsafe patterns before delegating to Tornado (lib/streamlit/web/server/app_static_file_handler.py:57-63).
  • Minor: error messaging accuracy: ComponentPathUtils._assert_relative_no_traversal distinguishes traversal vs “absolute” by scanning segments (lib/streamlit/components/v2/component_path_utils.py:149-160), but non-traversal unsafe inputs (e.g. null bytes) will still raise “Absolute paths…” which can be misleading.
  • Minor: redundancy/clarity in validator: is_unsafe_path_pattern checks both path.startswith(("\\", "/")) and os.path.isabs(path) (lib/streamlit/path_security.py:86-92). This is fine, but the combination is slightly redundant and may benefit from a brief comment explaining why both are retained (or remove one if not needed).

Test Coverage

  • Unit tests are strong and mostly follow repo guidance:
    • New dedicated tests for the shared validator in lib/tests/streamlit/path_security_test.py (typed, docstrings, paramized coverage).
    • Good regression test asserting realpath() is not called for unsafe input (lib/tests/streamlit/web/server/component_file_utils_test.py:253-272), which directly encodes the SSRF security invariant.
    • Good integration coverage for Tornado handler via HTTP-level tests (lib/tests/streamlit/web/server/app_static_file_handler_test.py:245-286).
  • No e2e tests added: That seems appropriate here since the change is backend path validation + handler behavior; unit/integration tests cover the critical invariant well.
  • One small best-practice note: lib/tests/streamlit/web/server/app_static_file_handler_test.py is unittest-based and many pre-existing test methods are untyped; the newly added methods are typed, which is good, but the file overall doesn’t fully match the “new tests should be fully annotated” guideline. I wouldn’t block on this since it’s consistent with the existing test style in that file.

Backwards Compatibility

  • Intentional behavior tightening: build_safe_abspath now rejects paths containing .. segments even if they would normalize safely (e.g. sub/../inside.txt) (lib/tests/streamlit/web/server/component_file_utils_test.py:144-152). This is a behavioral change that could affect any callers that previously relied on passing normalized traversal segments.
    • Given the security context, this seems reasonable, but it’s worth calling out explicitly in the PR description/release notes if this endpoint behavior is user-observable (e.g. some apps or proxies generating such URLs).
  • HTTP status semantics: The static handler now returns 403 early for “unsafe patterns” (lib/streamlit/web/server/app_static_file_handler.py:57-63), where some cases may previously have been 404. This should be acceptable (and arguably preferable) for malicious inputs, but it is technically observable.

Security & Risk

  • Security improvement is high-signal and correctly targeted:
    • Prevents Windows from triggering SMB connections by ensuring UNC/drive-like paths are rejected before any realpath() resolution on attacker-controlled input (lib/streamlit/path_security.py:21-45, lib/streamlit/web/server/component_file_utils.py:57-63).
    • Extends similar hardening to Tornado’s static path resolution entrypoint (lib/streamlit/web/server/app_static_file_handler.py:57-63).
  • Low regression risk overall due to extensive unit coverage and the fact that the new checks are additive/early-return.
  • Potential edge-case tradeoff (acceptable): The validator treats leading backslashes as unsafe on all platforms (lib/streamlit/path_security.py:86-92), which could theoretically reject rare “valid” POSIX filenames that begin with \. In the context of URL/asset paths, this seems like a sensible defense-in-depth tradeoff.

Recommendations

  1. In ComponentPathUtils._assert_relative_no_traversal, consider returning a more precise error for non-traversal unsafe patterns (e.g. distinguish null byte / UNC / drive) instead of always falling back to “Absolute paths…” (lib/streamlit/components/v2/component_path_utils.py:149-160).
  2. Consider simplifying or documenting the overlapping absolute-path checks in is_unsafe_path_pattern (lib/streamlit/path_security.py:86-92) to make the intent clearer (e.g. why both prefix checks and os.path.isabs are needed).
  3. (Non-blocking) If this behavior change is user-observable, explicitly document the tightened rejection of normalized .. segments in the PR description/changelog notes (lib/tests/streamlit/web/server/component_file_utils_test.py:144-152).

Verdict

APPROVED: The security fix is well-scoped, defensively implemented (early validation), and backed by strong unit/integration tests with a key regression check that prevents reintroducing the SSRF vector.


This is an automated AI review using gpt-5.2-high. Please verify the feedback and use your judgment.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 2, 2026

Summary

This PR centralizes path validation into a shared security helper and applies it to component asset resolution and app static file handling, preventing UNC/drive-based SSRF and traversal patterns before filesystem operations. It also adds targeted unit tests to document the security invariants and regression risks.

Code Quality

The refactor is clear and well-scoped: the shared is_unsafe_path_pattern keeps behavior consistent across the codebase, and the new early checks are documented and placed before realpath/path resolution. The updated tests are organized and include negative assertions that align with our testing guidelines. No maintainability issues found.

Test Coverage

Unit tests were added for the new shared helper and for the component/static-file path handling, including regression coverage that ensures realpath is not called for unsafe inputs. No e2e coverage is needed here since behavior is server-side path validation with no UI changes. No frontend changes, so TypeScript test guidance is not applicable.

Backwards Compatibility

Paths containing .. segments are now rejected up front even if they would normalize inside the root. This is an intentional security hardening, but it is a behavior change that could affect unusual component asset references that rely on sub/../file style paths.

def build_safe_abspath(component_root: str, relative_url_path: str) -> str | None:
    r"""Build an absolute path inside ``component_root`` if safe.
    ...
    if is_unsafe_path_pattern(relative_url_path):
        return None
    ...

Security & Risk

Security posture is improved: SSRF/UNC path resolution is blocked before any filesystem operations, and explicit tests guard against regressions. No new security risks identified.

Recommendations

  1. No changes requested.

Verdict

APPROVED: Security hardening is solid with good unit coverage and no functional regressions observed.


This is an automated AI review using gpt-5.2-codex-high. Please verify the feedback and use your judgment.

@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nbellante/fix-unc-path-ssrf-vulnerability branch from 7e8adb2 to 29ac34d Compare February 2, 2026 21:51
@sfc-gh-nbellante sfc-gh-nbellante added ai-review If applied to PR or issue will run AI review workflow and removed ai-review If applied to PR or issue will run AI review workflow labels Feb 2, 2026
@sfc-gh-nbellante sfc-gh-nbellante removed the ai-review If applied to PR or issue will run AI review workflow label Feb 2, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Add path validation before os.path.realpath() calls to prevent Windows
UNC paths from triggering SMB connections, which could enable SSRF
attacks and NTLM hash disclosure.

Changes:
- Add _is_unsafe_path() helper to detect dangerous path patterns:
  - UNC paths (\\server\share, //server/share)
  - Windows drive letters (C:\, d:/)
  - Absolute paths (/etc/passwd, \rooted)
  - Path traversal (../ segments)
  - Null byte injection
- Call validation before any realpath() in build_safe_abspath()
- Add comprehensive tests for all unsafe path patterns

This fix protects all 5 endpoints using build_safe_abspath():
- ComponentRequestHandler.get()
- BidiComponentRequestHandler.get()
- _component_endpoint()
- _bidi_component_endpoint()
- _app_static_endpoint()
Address reviewer feedback: use consistent 400 Bad Request for all
malicious path requests instead of differentiating between 400 and
403. This makes error responses more opaque to attackers by not
revealing how far their request progressed.
Use string.ascii_letters instead of str.isalpha() to detect Windows drive
paths, avoiding false positives on valid POSIX filenames like 'Å:foo'.

Resolves comment 1
…tions

Change error from 'Absolute paths are not allowed' to 'Unsafe paths are not
allowed' to accurately describe rejections for null bytes, UNC paths, etc.

Resolves comment 2
Update Tornado component handlers to return 400 instead of 403 for unsafe
paths, matching Starlette behavior for consistent opacity across servers.

Resolves comment 3
@sfc-gh-nbellante sfc-gh-nbellante force-pushed the nbellante/fix-unc-path-ssrf-vulnerability branch from a0aa4e1 to 2af08ad Compare February 3, 2026 16:55
Copy link
Copy Markdown
Collaborator

@sfc-gh-bnisco sfc-gh-bnisco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the iterations on this 👍

@sfc-gh-nbellante sfc-gh-nbellante merged commit 934d2f1 into develop Feb 3, 2026
43 of 44 checks passed
@sfc-gh-nbellante sfc-gh-nbellante deleted the nbellante/fix-unc-path-ssrf-vulnerability branch February 3, 2026 17:55
github-actions bot pushed a commit that referenced this pull request Feb 3, 2026
…handling (#13733)

## Describe your changes

Added centralized path security validation to prevent Server-Side
Request Forgery (SSRF) and path traversal vulnerabilities. The changes
include:

1. Created a new `path_security.py` module with
`is_unsafe_path_pattern()` function that detects potentially dangerous
paths:
   - UNC paths (Windows network shares)
   - Absolute paths (Windows drive letters or root-based paths)
   - Path traversal attempts using `..`
   - Null byte injection attempts
   - Windows special path prefixes

2. Added a new `PathSecurityMiddleware` for Starlette that blocks unsafe
paths at the HTTP layer before any filesystem operations.

3. Updated component path handling to validate paths BEFORE calling
`os.path.realpath()`, preventing Windows from triggering SMB connections
to attacker-controlled servers.

4. Modified `AppStaticFileHandler` to check for unsafe paths before
filesystem operations.

5. Standardized security responses across the codebase (400 Bad Request
for malicious paths).

## Testing Plan

Added comprehensive test cases covering:
- Detection of various unsafe path patterns
- Validation of UNC paths and Windows drive letters
- Handling of URL-decoded paths
- Null byte injection attempts
- Windows special path prefixes
- Mixed path separator handling
- Verification that legitimate paths still work correctly
- Middleware positioning and integration with the request pipeline
- Proper handling of WebSocket connections

The tests verify both the new security functions and their integration
across different parts of the codebase.

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this
project are made under the Apache 2.0 license.
sfc-gh-nbellante added a commit that referenced this pull request Feb 4, 2026
The path security middleware added in #13733 intercepts double-slash
paths earlier in the request pipeline and returns 400 Bad Request
instead of the previous 403 Forbidden from the route handler.
sfc-gh-nbellante added a commit that referenced this pull request Feb 4, 2026
…do and Starlette (#13822)

## Summary
- Added `UnsafePathBlockHandler` to the Tornado server that blocks
double-slash paths with `400 Bad Request`, matching the Starlette
`PathSecurityMiddleware` behavior
- Updated the E2E test `test_double_slash_not_redirected_to_external` to
expect `400` instead of `403`
- Previously, the Starlette middleware (from #13733) returned 400 while
Tornado relied on its built-in handling which returned 403, causing the
test to fail depending on which server backend was used

### Files changed:
- `lib/streamlit/web/server/routes.py` — Added `UnsafePathBlockHandler`
class
- `lib/streamlit/web/server/server.py` — Registered the handler as the
first route
- `e2e_playwright/web_server_test.py` — Updated test to expect 400

## Test plan
- [ ] `web_server_test.py::test_double_slash_not_redirected_to_external`
passes for both Tornado and Starlette E2E test suites
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:security Related to security concerns change:bugfix PR contains bug fix implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants