Cybersecurity Leader specializing in threat hunting, penetration testing, and purple team operations. I detect adversary activity early and drive the mitigation and remediation work that closes the gap before it becomes an incident. Skilled in deploying and tuning endpoint detection and response platforms to strengthen visibility across complex environments. My expertise extends into data loss prevention, AI and LLM risk assessment, and governance mapped to frameworks including MITRE ATT&CK, MITRE ATLAS, and the OWASP LLM Top 10. I study how adversaries exploit systems, then translate those findings into the controls, policies, and risk decisions organizations need to operate securely.
Sole security professional carrying company-wide responsibility across penetration testing, AI governance, and data protection.
Owns company-wide penetration testing, AI governance, and tool-approval authority, with primary responsibility for the organization's data loss prevention program and oversight of managed detection and response operations.
Peer-reviewed contributions to the information security field.
Introduces a text steganography method that hides data within justified PDF text by exploiting the variable spacing text editors insert to remove ragged edges. The secret message is compressed with Huffman coding, then embedded by selectively replacing justification spaces with normal spaces across chosen host lines, with the scheme keyed for each use to strengthen communication security. Compared to prior text-based steganography approaches, the method embeds a higher information payload without altering the cover file's size, requires no electronic file exchange between parties, and remains recoverable even from a printed copy.
Offensive security and AI security, backed by hands-on lab work and applied engagement experience.
Original perspectives, vulnerability research, and threat hunting notes from the field, not case studies of confidential work.
Data loss prevention was built for a world where humans moved data — copying a file, attaching a document to an email, uploading to a personal drive. Every major DLP program in production today still assumes that model. But that world is gone. AI copilots now read entire mailboxes to draft a reply. Agentic tools summarize confidential documents on request. Employees paste proprietary code into public LLM chat windows without a second thought. None of this looks like the exfiltration patterns legacy DLP was designed to catch, and most organizations are only starting to notice the gap.
The problem isn't the AI tools. It's the missing foundation underneath them. You cannot protect what you haven't classified. Before any policy, any blocking rule, any endpoint control can work, an organization needs a real answer to a basic question: what is this data, and how sensitive is it? Most companies adopting AI tools today don't have that answer at scale. Labels are inconsistent, ownership is unclear, and sensitive data sits mixed in with everything else — which means AI tools reading "all available context" are, by definition, reading things they shouldn't.
Classification has to come first. Not as a compliance checkbox, but as living infrastructure — data labeled consistently at creation, ownership assigned, sensitivity tiers that actually mean something to the tools enforcing them downstream. Skip this step and every control built on top of it is guessing.
Then protection has to be layered, not singular. No single control catches everything an AI-augmented workflow can do with data. Classification tells you what matters. Endpoint policy governs what a device or application is allowed to do with it. Network and cloud monitoring catch what slips past both. Each layer exists because the others will eventually fail or be bypassed — by a misconfigured integration, a compromised account, or simply a tool doing exactly what it was asked to do with data it was never meant to see.
This is the shift security leaders need to make: DLP is no longer a tool you deploy once. It's a foundation you maintain continuously, because the definition of "movement" now includes an AI model reading, summarizing, and acting on data — not just a person sending it somewhere.
The organizations that get ahead of this aren't the ones with the most tools. They're the ones who classify first, control second, and monitor third — in that order, every time.
TL;DR
CVE-2025-2945 is a critical remote code execution vulnerability in pgAdmin 4, the most widely used open-source administration platform for PostgreSQL. An authenticated user could turn a simple boolean flag in the Query Tool into arbitrary Python code execution on the server, because the flag was being parsed with Python's eval() instead of an actual boolean check. The fix, once you see it, is a single line, but the path to that line says a lot about how RCE vulnerabilities are actually born: not from exotic deserialization chains, but from a developer reaching for a quick shortcut that happened to also be a code execution primitive.
Background
pgAdmin 4 is the de facto standard web interface for managing PostgreSQL databases, used by DBAs, developers, and platform teams to run queries, inspect schemas, and administer production data through a browser instead of a terminal. Its Query Tool is the core of that experience: a SQL editor that tracks transaction state (has this query been committed? is autocommit on?) and ships that state back and forth between the JavaScript frontend and the Python/Flask backend on every request. That transaction-state handshake is exactly where this vulnerability lived. Any interface that manages live database transactions for an authenticated user is, almost by definition, a high-value target: it sits adjacent to the data it's meant to protect, and it's usually reachable from wherever the DBA's browser is, which in production environments is often broader network access than the database itself allows.
Root Cause
The vulnerable code lived in web/pgadmin/tools/sqleditor/__init__.py, inside start_query_download_tool():
# Vulnerable (pgAdmin <= 9.1)
if key == 'query_commited':
query_commited = (
eval(value) if isinstance(value, str) else value
)
query_commited is meant to be a simple boolean: did the frontend already commit this transaction, yes or no. The frontend sends it as a string, "true" or "false", and the backend needed to turn that string into a Python bool. Instead of writing an explicit check, the code passed the string straight into eval().
This is a subtly different failure than most public writeups describe. It's not deserializing a complex object and not processing a JSON payload; it's using eval() as a lazy type-coercion shortcut, on the (false) assumption that the input would only ever be the literal string "true" or "false". Since eval() executes as Python, not as a boolean parser, any string is fair game: __import__('os').system('id') evaluates exactly as validly as True does.
The same anti-pattern appeared a second time in web/pgacloud/providers/google.py, inside the Cloud Deployment module's Google provider:
# Vulnerable
high_availability = (
'REGIONAL' if eval(args.high_availability) else 'ZONAL'
)
Same shape, same root cause: a value that should have been coerced to a boolean was evaluated as code instead. Two endpoints, one underlying mistake, made independently by different code paths, which is itself worth noting, since it suggests the pattern wasn't a one-off typo but something close to a house convention for handling stringly-typed booleans.
The fix, shipped in pgAdmin 9.2, replaced eval() with an actual string comparison in both places:
# Fixed (pgAdmin 9.2+)
query_commited = (
value.lower() in ('true', '1') if isinstance(value, str) else value
)
No parser, no library, no complexity, just doing the boolean check the code should have done from the start.
Exploitation Logic
The vulnerability is authenticated: an attacker needs a valid pgAdmin session before reaching the vulnerable code path. That requirement matters for accurate risk assessment: this is not a pre-auth RCE, and its CVSS 9.9 score reflects the severity of impact, not ease of unauthenticated access. Given how routinely admin interfaces end up secured with weak or default credentials, though, "authenticated" is a much lower bar in practice than it sounds on paper.
Once authenticated, reaching start_query_download_tool() requires an active Query Tool transaction. pgAdmin's SQL editor is namespaced by a transaction ID, and that transaction has to be bound to a registered server and database connection before the download endpoint will process a request at all. This isn't an incidental detail; it means an attacker needs at least one database server already registered in the target pgAdmin instance. In deployments where pgAdmin is pre-configured with one or more saved server connections, which is extremely common in real environments, this precondition is trivially met.
From there, the attacker submits a POST request to /sqleditor/query_tool/download/<trans_id> with a crafted query_commited value, not "true" or "false", but a Python expression with a side effect. The backend hands that string to eval() and executes it under the privileges of the pgAdmin service process. The Cloud Deployment path follows the identical logic through /cloud/deploy and the high_availability parameter, for accounts with access to that module.
Vulnerability Class
This is CWE-95 (Eval Injection), but it's worth being precise about which flavor. The more commonly discussed eval-injection bugs involve deserializing attacker-controlled objects (think Python pickle, or eval() used to parse what should have been JSON). This one is narrower and, in some ways, more mundane: eval() used purely as a boolean type-coercion shortcut. It's the same class of mistake that shows up whenever a developer reaches for eval() to parse a stringly-typed flag from a query string or form field instead of writing value.lower() == 'true', a one-line fix that's easy to skip past in code review because the call site looks so small.
Detection Guidance
Defenders monitoring pgAdmin deployments should watch for:
/sqleditor/query_tool/download/<trans_id> or /cloud/deploy where the query_commited or high_availability parameter is anything other than a literal "true", "false", "1", or "0".__import__, os.system, subprocess, eval, exec, open(, inside those parameter values. This is a near-certain exploitation signature; there's no legitimate reason for a boolean field to contain any of these strings./bin/sh, /bin/bash) or network utilities (nc, curl, wget): the pgAdmin process has no legitimate reason to spawn either.Remediation
eval() reached for as a coercion shortcut rarely announces itself as dangerous in code review.MITRE ATT&CK Mapping
eval() injection serves as the initial access/execution vector once authenticated.References