pypi package report

Is mechanicalsoup safe?

1 known vulnerability, worst severity HIGH.

// reach

0 direct dependencies

none carry a known advisory

    1 packages depend on it

    an advisory here reaches each of them

    Create a free accountfor every dependency path, dependent and what to upgrade
    // ai model usage

    No published models are known to use this package.


    cvss
    0.0
    medium

    severity out of 10

    epss
    0.00%
    medium

    chance of exploitation in 30 days, 65th percentile of all CVEs

    xyz score
    0.0
    low

    CyberXYZ composite out of 10

    fig. 01 — GHSA-x456-3ccm-m6j4, the advisory selected below

    // 1 advisories

    GHSA-x456-3ccm-m6j4

    HIGHCVE-2023-34457
    // summary

    A malicious web server can read arbitrary files on the client using a inside HTML form.

    // details

    This affects the extremely common pattern of form submission:

    b = mechanicalsoup.StatefulBrowser()
    b.select_form(...)
    b.submit_selected()

    The problem is with the code in browser.Browser.getrequestkwargs:

        if tag.get("type", "").lower() == "file" and multipart:
            filepath = value
            if filepath != "" and isinstance(filepath, str):
                content = open(filepath, "rb")
            else:
                content = ""
            filename = os.path.basename(filepath)
            # If value is the empty string, we still pass it
            # for consistency with browsers (see
            # https://github.com/MechanicalSoup/MechanicalSoup/issues/250).
            files[name] = (filename, content)

    The file path is taken from the bs4 tag "value" attribute. However, this path will default to whatever the server sends. So if a malicious web server were to send something like:

      
        
        
      

    then upon .submitselected() the mechanicalsoup browser will happily send over the contents of your SSH private key.

    // poc
    import attr
    import mechanicalsoup
    import requests
    
    
    class NevermindError(Exception):
        pass
    
    
    @attr.s
    class FakeSession:
        session = attr.ib()
    
        headers = property(lambda self: self.session.headers)
    
        def request(self, *args, **kwargs):
            print("requested", args, kwargs)
            raise NevermindError  # don't actually send request
    
    
    def demonstrate(inputs=None):
        b = mechanicalsoup.StatefulBrowser(FakeSession(requests.Session()))
        b.open_fake_page("""\
    
    
    
    
    
    
    
    """, url="http://127.0.0.1:9/")
        b.select_form()
        if inputs is not None:
            b.form.set_input(inputs)
        try:
            b.submit_selected()
        except NevermindError:
            pass
    
    # %%
    
    # unpatched
    demonstrate()
    # OUTPUT: requested () {'method': 'post', 'url': 'http://127.0.0.1:9/', 'files': {'evil': ('passwd', ), 'second': ('', '')}, 'headers': {'Referer': 'http://127.0.0.1:9/'}, 'data': [('greeting', 'hello')]}
    
    # %%
    
    # with the patch, this now works. users MUST open the file manually and
    # use browser.set_input() using the file object.
    demonstrate({"greeting": "hiya", "evil": open("/etc/hostname", "rb").name, "second": open("/dev/null", "rb")})
    # OUTPUT: requested () {'method': 'post', 'url': 'http://127.0.0.1:9/', 'files': {'evil': ('hostname', ), 'second': ('null', )}, 'headers': {'Referer': 'http://127.0.0.1:9/'}, 'data': [('greeting', 'hiya')]}
    
    # %%
    
    # with the patch, this raises a ValueError with a helpful string
    demonstrate({"evil": "/etc/hostname"})
    
    # %%
    
    # with the patch, we silently send no file if a malicious server tries the attack:
    demonstrate()
    // suggested patch
    diff --git a/mechanicalsoup/browser.py b/mechanicalsoup/browser.py
    index 285f8bb..68bc65e 100644
    --- a/mechanicalsoup/browser.py
    +++ b/mechanicalsoup/browser.py
    @@ -1,7 +1,8 @@
    +import io
     import os
     import tempfile
     import urllib
     import weakref
     import webbrowser
     
     import bs4
    @@ -227,15 +228,21 @@ class Browser:
                         value = tag.get("value", "")
     
                     # If the enctype is not multipart, the filename is put in
                     # the form as a text input and the file is not sent.
                     if tag.get("type", "").lower() == "file" and multipart:
                         filepath = value
                         if filepath != "" and isinstance(filepath, str):
    -                        content = open(filepath, "rb")
    +                        content = getattr(tag, "_mechanicalsoup_file", None)
    +                        if content is False:
    +                            raise ValueError(
    +                                """From v1.3.0 onwards, you must pass an open file object directly, for example using `form.set_input({"name": open("/path/to/filename", "rb")})`. This change is to mitigate a security vulnerability where a malicious web server could read arbitrary files from the client."""
    +                            )
    +                        elif not isinstance(content, io.IOBase):
    +                            content = ""
                         else:
                             content = ""
                         filename = os.path.basename(filepath)
                         # If value is the empty string, we still pass it
                         # for consistency with browsers (see
                         # https://github.com/MechanicalSoup/MechanicalSoup/issues/250).
                         files[name] = (filename, content)
    diff --git a/mechanicalsoup/form.py b/mechanicalsoup/form.py
    index a67195c..82f6015 100644
    --- a/mechanicalsoup/form.py
    +++ b/mechanicalsoup/form.py
    @@ -1,8 +1,9 @@
     import copy
    +import io
     import warnings
     
     from bs4 import BeautifulSoup
    
    // impact

    All users of MechanicalSoup's form submission are affected, unless they took very specific (and manual) steps to reset HTML form field values.

    // cvss v3.1 vector

    CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N

    Attack vector
    Network
    Attack complexity
    High
    Privileges required
    None
    User interaction
    None
    Scope
    Unchanged
    Confidentiality
    High
    Integrity
    None
    Availability
    None

    Checked 2026-10-23 at 18:55 UTC. The most recent advisory here was published 2023-07-05. Updated continuously from NVD, GHSA, OSV and CNA feeds.

    Think a verdict here is wrong? Tell us — we respond within 2 business days.