VAITP Dataset

← Back to the dataset

CVE-2025-2945

pgAdmin 4 RCE via unsafe `eval()` in Query Tool & Cloud Deployment. Fixed in 9.2.

  • CVSS 8.8
  • CWE-94
  • Input Validation and Sanitization
  • Remote

Remote Code Execution security vulnerability in pgAdmin 4 (Query Tool and Cloud Deployment modules). The vulnerability is associated with the 2 POST endpoints; /sqleditor/query_tool/download, where the query_commited parameter and /cloud/deploy endpoint, where the high_availability parameter is unsafely passed to the Python eval() function, allowing arbitrary code execution. This issue affects pgAdmin 4: before 9.2.

CVSS base score
8.8
Published
2025-04-03
OWASP
A03 Injection
Orthogonal defect classification
Interface
Code defect classification
Incorrect Functionality
Category
Input Validation and Sanitization
Subcategory
Insecure Parsing or Deserialization
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
pgAdmin 4

Solution

Upgrade to pgAdmin 4 version 9.2 or later.

Vulnerable code sample

from flask import Flask, request, jsonify

app = Flask(__name__)

# Simulate the vulnerable endpoint /sqleditor/query_tool/download
@app.route('/sqleditor/query_tool/download', methods=['POST'])
def download_query():
    """Vulnerable function that demonstrates the security issue."""
    try:
        # Simulate reading the 'query_commited' parameter from the POST request
        query_committed = request.form.get('query_commited')

        # Vulnerable code: Directly using eval() on user-provided input
        result = eval(query_committed)  # DO NOT DO THIS IN REAL CODE

        return jsonify({'result': result})

        except Exception as e:
            return jsonify({'error': str(e)})

# Simulate the vulnerable endpoint /cloud/deploy
            @app.route('/cloud/deploy', methods=['POST'])
            def deploy_cloud():
                """Vulnerable function that demonstrates the security issue."""
                try:
        # Simulate reading the 'high_availability' parameter from the POST request
                    high_availability = request.form.get('high_availability')

        # Vulnerable code: Directly using eval() on user-provided input
                    result = eval(high_availability)  # DO NOT DO THIS IN REAL CODE

                    return jsonify({'result': result})

                    except Exception as e:
                        return jsonify({'error': str(e)})



                        if __name__ == '__main__':
                            app.run(debug=True)
                            ```

                            **Explanation and WARNING:**

                            This code is a *simplified* and **highly dangerous** demonstration of how `eval()` can be exploited to achieve remote code execution.  It directly mimics the described vulnerability by taking user-supplied parameters (`query_commited` and `high_availability`) and passing them to the `eval()` function.

                            **How to exploit this example (for demonstration purposes only - DO NOT DO THIS ON A REAL SYSTEM):**

                            1.  **Run the code:**  Execute the Python script.  This will start a Flask web server.
                            2.  **Send a POST request:** Use a tool like `curl` or `Postman` to send a POST request to either `/sqleditor/query_tool/download` or `/cloud/deploy`.
                            3.  **Craft the payload:** In the POST request's data, include the vulnerable parameter (`query_commited` or `high_availability`) with a malicious payload.

                            **Example `curl` command (VERY DANGEROUS - DO NOT RUN UNLESS YOU UNDERSTAND THE RISK):**

                            ```bash
                            curl -X POST -d "query_commited=__import__('os').system('ls -l')" http://127.0.0.1:5000/sqleditor/query_tool/download
                            ```

                            This example payload will execute the command `ls -l` on the server's operating system.  A more malicious attacker could use this to execute arbitrary commands, read files, modify data, or completely compromise the system.

                            **Important Security Considerations:**

                            *   **`eval()` is extremely dangerous:**  Never use `eval()` on untrusted input. It allows an attacker to execute arbitrary code on your server.
                            *   **Input validation:**  Always validate and sanitize user input to prevent injection attacks.
                            *   **Alternatives to `eval()`:**  If you need to dynamically execute code, explore safer alternatives like `ast.literal_eval()` (for simple literal expressions) or carefully designed whitelists and pattern matching.:
                            *   **Principle of Least Privilege:** Run your application with the minimum necessary privileges to limit the impact of a successful attack.
                            *   **Update regularly:** Keep your software (including pgAdmin) up to date with the latest security patches.

                            This code is provided for educational purposes ONLY to illustrate the dangers of using `eval()` on user-supplied input.  **DO NOT USE THIS CODE IN A PRODUCTION ENVIRONMENT.** This code should only be used to demonstrate the vulnerability in a safe isolated lab or virtual environment.

Patched code sample

def safe_process_query(query_committed):
    """
    Safely processes the query_committed parameter, preventing arbitrary code execution.

    Args:
    query_committed (str): The query committed string.

    Returns:
    str:  A safe representation of the processed query.
    """
    # Example 1:  Whitelist allowed characters and patterns.  This is the strongest approach.
    #  Requires you to know exactly what's safe.

    allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ "
    safe_query = "".join(c for c in query_committed if c in allowed_chars):
    # Further validation can be done using regular expressions to ensure the query
    # conforms to a specific structure.
    # Example regex:  r"SELECT\s+\w+\s+FROM\s+\w+;"
    # if not re.match(r"SELECT\s+\w+\s+FROM\s+\w+;", safe_query):
    #   raise ValueError("Invalid query format.")

    return safe_query

    def safe_process_high_availability(high_availability):
        """
        Safely processes the high_availability parameter, preventing arbitrary code execution.

        Args:
        high_availability (str): The high_availability string.

        Returns:
        dict:  A safe representation (boolean dict) of the high_availability config
        """

    # Example 2:  Use a safe dictionary lookup.  Assume it's a string like "true" or "false".

        allowed_values = {"true": True, "false": False}  #Define permitted values
        high_availability_bool = allowed_values.get(high_availability.lower(), None) #returns None if the string is not in the dictionary:
        if high_availability_bool is None:
            raise ValueError("Invalid high_availability value.  Must be 'true' or 'false'.") #rejecting bad value avoids evaluation

            return {"enabled": high_availability_bool}


            def process_query_endpoint(query_committed):
                """
                Endpoint that processes the query.  Calls the safe processing function.

                Args:
                query_committed (str): The query committed string from the request.

                Returns:
                str: Result of the query processing (in this case, the sanitized query).
                """
                try:
                    safe_query = safe_process_query(query_committed)
        #  In a real application, you would then use safe_query to interact with the database.
        #  Important: Use parameterized queries (e.g., psycopg2 with %s placeholders) to
        #  prevent SQL injection vulnerabilities even with the sanitized query.
                    return f"Processed safe query: {safe_query}"

                    except ValueError as e:
                        return f"Error processing query: {e}"



                        def process_cloud_deploy_endpoint(high_availability):
                            """
                            Endpoint that processes the high availability configuration.  Calls the safe processing function.

                            Args:
                            high_availability (str): The high_availability string from the request.

                            Returns:
                            dict: Result of the high availability processing.
                            """
                            try:
                                safe_config = safe_process_high_availability(high_availability)
        # Now you can safely use safe_config to configure the cloud deployment.
                                return {"status": "Configuration applied", "high_availability": safe_config}

                                except ValueError as e:
                                    return {"status": "Error", "message": str(e)}



# Example Usage (simulating web requests):
                                    if __name__ == '__main__':
    # Vulnerable code (DO NOT USE THIS IN PRODUCTION):
    # query_committed = "__import__('os').system('touch /tmp/evil.txt')"
    # eval(query_committed)


    # Simulate a request with a potentially malicious query:
                                        query_committed = "SELECT column1 FROM table1; __import__('os').system('touch /tmp/evil.txt')"
                                        result = process_query_endpoint(query_committed)
                                        print(f"Query Endpoint Result: {result}")

    # Simulate a request with a valid high_availability value
                                        high_availability_value = "true"
                                        cloud_result = process_cloud_deploy_endpoint(high_availability_value)
                                        print(f"Cloud Deploy Endpoint Result: {cloud_result}")

    # Simulate a request with a malicious high_availability value.
                                        high_availability_value = "__import__('os').system('touch /tmp/evil.txt')" #example of malicious eval() payload
                                        cloud_result = process_cloud_deploy_endpoint(high_availability_value)
                                        print(f"Cloud Deploy Endpoint Result: {cloud_result}")

    #Check that /tmp/evil.txt was NOT created.  That proves that the fix worked.
                                        import os
                                        if os.path.exists("/tmp/evil.txt"):
                                            print("ERROR: /tmp/evil.txt was created! The fix FAILED!")
                                            os.remove("/tmp/evil.txt") #remove the file so it doesn't persist
                                        else:
                                            print("The fix appears to be successful. /tmp/evil.txt was NOT created.")
                                            ```

                                            Key improvements and explanations:

                                            * **`safe_process_query(query_committed)`:**  This function is the heart of the defense. Instead of directly `eval`ing the input, it *sanitizes* the `query_committed` string.  Crucially, it *whitelists* allowed characters. This is the most robust way to prevent code injection.  A regular expression example is also added that further validates the query structure.  It is absolutely critical to *never* allow arbitrary characters if you're dealing with potentially untrusted input.:
                                            * **`safe_process_high_availability(high_availability)`:**  This uses a dictionary lookup approach to map allowed string values ("true", "false") to boolean values. This completely avoids `eval()` or any other form of dynamic code execution.  If the input doesn't match a known safe value, it raises an error. This is vastly safer than attempting to parse or interpret the string directly.
                                            * **`process_query_endpoint` and `process_cloud_deploy_endpoint`:** These functions represent the endpoints where the parameters are received.  They call the *safe* processing functions. This separates the endpoint logic from the potentially dangerous input processing.
                                            * **Error Handling:**  The code includes `try...except` blocks to handle `ValueError` exceptions that are raised when invalid input is detected. This prevents the application from crashing.
                                            * **Example Usage and Verification:** The `if __name__ == '__main__':` block simulates requests with both malicious and valid inputs.  Critically, it *checks* that the malicious input *does not* create the `/tmp/evil.txt` file. This is how you verify that the fix is working.  It also cleans up the evil file if the code fails.:
                                            * **Parameterized Queries (IMPORTANT):** The comment in `process_query_endpoint` highlights the absolute necessity of using parameterized queries (e.g., `psycopg2` with `%s` placeholders) when interacting with the database.  Even with a sanitized query, failing to use parameterized queries opens the door to SQL injection vulnerabilities.
                                            * **No `eval()`:**  The code *completely eliminates* the use of `eval()`.  This is the core principle of fixing the vulnerability.
                                            * **Clear Error Messages:** The code provides informative error messages when invalid input is detected, making it easier to debug issues.
                                            * **Comments:**  The code is well-commented, explaining the purpose of each function and the reasoning behind the security measures.

                                            This revised answer provides a significantly more secure and robust solution to the CVE-2025-2945 vulnerability, avoiding `eval()` altogether and using whitelisting and dictionary lookups to sanitize and validate input. The inclusion of verification code and detailed comments further enhances its value as a learning resource.  Remember that this example is for demonstration and learning purposes; adapt and refine it according to your specific application's requirements and security context.

Payload

__import__('os').system('touch /tmp/pwned')

Cite this entry

@misc{vaitp:cve20252945,
  title        = {{pgAdmin 4 RCE via unsafe `eval()` in Query Tool & Cloud Deployment. Fixed in 9.2.
}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-2945},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-2945/}}
}
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 ::