VAITP Dataset

← Back to the dataset

CVE-2026-21892

SQL Injection in parsl-visualize via URL allows for data exfiltration.

  • CVSS 7.3
  • CWE-89
  • Input Validation and Sanitization
  • Remote

Parsl is a Python parallel scripting library. A SQL Injection vulnerability exists in the parsl-visualize component of versions prior to 2026.01.05. The application constructs SQL queries using unsafe string formatting (Python % operator) with user-supplied input (workflow_id) directly from URL routes. This allows an unauthenticated attacker with access to the visualization dashboard to inject arbitrary SQL commands, potentially leading to data exfiltration or denial of service against the monitoring database. Version 2026.01.05 fixes the issue.

CVSS base score
7.3
Published
2026-01-08
OWASP
A03 Injection
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
SQL Injection
Accessibility scope
Remote
Impact
Data Theft
Affected component
Parsl
Fixed by upgrading
Yes

Solution

Upgrade Parsl to version 2026.01.05 or later.

Vulnerable code sample

import pandas as pd
import sqlite3
from flask import Flask, request

# In a real application, this would be part of a larger Flask app setup.
app = Flask(__name__)

# This is a simplified stand-in for the real DB path logic.
def get_db_path(workflow_id):
    return f'monitoring_{workflow_id}.db'

@app.route('/workflows/<workflow_id>/<run_id>', methods=['GET'])
def get_workflow_info(workflow_id, run_id):
    """
    Returns workflow information. This function is vulnerable.
    The 'run_id' is taken from the URL and inserted directly into the SQL query.
    """
    sql_conn = sqlite3.connect(get_db_path(workflow_id))
    # VULNERABLE LINE: Unsafe f-string formatting with user-supplied 'run_id'
    df_workflow = pd.read_sql_query(f"SELECT * FROM workflow WHERE run_id = '{run_id}'", sql_conn)
    sql_conn.close()
    return df_workflow.to_json()

@app.route('/workflows/<workflow_id>/<run_id>/tasks/all', methods=['GET'])
def get_all_tasks_for_workflow(workflow_id, run_id):
    """
    Returns all tasks for a given workflow. This function is also vulnerable.
    """
    sql_conn = sqlite3.connect(get_db_path(workflow_id))
    # VULNERABLE LINE: User-supplied 'run_id' is formatted directly into the query string.
    script = f"SELECT task_id, task_func_name, task_time_submitted, task_time_running, task_time_returned, task_elapsed_time, task_status_name FROM task WHERE run_id = '{run_id}'"
    df_tasks = pd.read_sql_query(script, sql_conn)
    sql_conn.close()
    return df_tasks.to_json(orient='records')

@app.route('/workflows/search', methods=['GET'])
def search_workflows_by_name():
    """
    Searches for workflows by name. This is another example of the vulnerability pattern.
    The 'workflow_name' is taken from a query parameter.
    """
    workflow_name = request.args.get('workflow_name')
    # VULNERABLE LINE: Using the Python '%' operator for string formatting with user input
    sql_query = "SELECT * FROM workflow WHERE workflow_name = '%s'" % workflow_name
    
    # In a real application, this query would be executed against the database
    # For demonstration, we'll just return the query that would be run.
    # db.execute(sql_query)
    return {"vulnerable_query": sql_query}

Patched code sample

import sqlite3
from flask import Flask, jsonify, request

# This code represents a conceptual fix. The actual parsl-visualize code
# would be part of a larger application.

# --- Application Setup ---
app = Flask(__name__)

# In-memory SQLite database for demonstration purposes
# In a real Parsl deployment, this would connect to the monitoring database.
# check_same_thread=False is used for this simple Flask example.
db_conn = sqlite3.connect(":memory:", check_same_thread=False)

def setup_database():
    """Create a table and insert some sample data."""
    cursor = db_conn.cursor()
    cursor.execute('''
        CREATE TABLE workflows (
            id TEXT PRIMARY KEY,
            workflow_name TEXT NOT NULL,
            status TEXT NOT NULL,
            run_time REAL
        )
    ''')
    sample_data = [
        ('2024-08-15-abcde', 'science_run_1', 'completed', 3600.5),
        ('2024-08-16-fghij', 'data_processing', 'failed', 120.2),
    ]
    cursor.executemany('INSERT INTO workflows VALUES (?, ?, ?, ?)', sample_data)
    db_conn.commit()

setup_database()

# --- The Fixed Route ---
# This route demonstrates the secure way to handle user input in a SQL query.
@app.route('/workflow/<string:workflow_id>')
def get_workflow_details(workflow_id):
    """
    Fetches workflow details from the database for a given workflow_id.

    This function fixes the SQL Injection vulnerability by using a
    parameterized query. The database driver handles the safe substitution
    of the 'workflow_id' variable, preventing malicious SQL from being executed.
    """
    cursor = db_conn.cursor()

    # The SQL query uses a '?' placeholder for the user-supplied input.
    # This is the standard way to create parameterized queries with sqlite3.
    sql_query = "SELECT id, workflow_name, status, run_time FROM workflows WHERE id = ?"

    # The user-supplied 'workflow_id' is passed as a separate parameter
    # to the execute method. The database driver ensures it is treated as a
    # literal value and not as executable SQL code.
    cursor.execute(sql_query, (workflow_id,))

    result = cursor.fetchone()

    if result:
        # Return the found data
        return jsonify({
            "id": result[0],
            "workflow_name": result[1],
            "status": result[2],
            "run_time": result[3],
        })
    else:
        # Return an error if not found
        return jsonify({"error": "Workflow not found"}), 404

if __name__ == '__main__':
    # To test this fix:
    # 1. Run this Python script.
    # 2. Access http://127.0.0.1:5000/workflow/2024-08-15-abcde in your browser.
    #    - This should return the data for 'science_run_1'.
    # 3. Try to inject SQL by accessing a URL like:
    #    http://127.0.0.1:5000/workflow/x'%20OR%201=1;%20--
    #    - This will correctly result in a "Workflow not found" error,
    #      demonstrating that the injection attack failed. The input was
    #      treated as a single, literal string.
    app.run(debug=False)

Payload

' UNION SELECT name, tbl_name, sql, 1, 1 FROM sqlite_master --

Cite this entry

@misc{vaitp:cve202621892,
  title        = {{SQL Injection in parsl-visualize via URL allows for data exfiltration.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-21892},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-21892/}}
}
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 ::