Bypassing an AI platform’s Python sandbox with two lines of SQL

Cube de confinement transparent fissuré protégeant un composant IA, avec des fichiers qui s’en échappent — Defensy

During a recent web application pentest against an AI productivity platform, our team escaped the code-execution sandbox of one of its AI agents and read more than forty backend source files — including the AWS account ID, the bootstrap super-admin email and the full map of the platform’s secrets. No exploit, no special privilege: a free-tier account and two lines of SQL were enough. This anonymized use case explains how, and the few configuration lines that close the gap.

Key takeaways
  • A Python sandbox based on RestrictedPython blocked dangerous imports, but had no visibility into a native C++ library (DuckDB) that was pre-loaded before the restrictions were applied.
  • DuckDB’s filesystem functions (glob(), read_text()) read the application source directly, because the sandbox ran in the production code directory.
  • The data was exfiltrated through the agent’s own “save file” tool — no external server, no outbound callback.
  • Three settings fix it: enable_external_access=false, lock_configuration=true, and an isolated working directory.
EngagementWeb application pentest
TargetAI productivity platform (anonymized)
VectorSandbox escape via a pre-loaded library
Impact40+ source files, full credential map exposed
StatusFixed — coordinated disclosure

The setup: an AI agent with a code tool

The platform lets users talk to specialized AI agents. One of them, an analysis agent, exposed an execute_code tool that ran user-supplied Python for data work, inside a sandbox built on RestrictedPython. The tool even advertised its environment: “available libraries: pandas, numpy, matplotlib, duckdb”. DuckDB was pre-loaded and ready to use — and that is exactly where the problem started.

Why RestrictedPython was not enough

RestrictedPython rewrites user code through a custom AST transformer and runs it with a restricted set of globals. Importing os, subprocess or socket, or calling open() directly, is blocked. It is a Python-layer control: it can only intercept what passes through the Python import machinery and the abstract syntax tree.

What it cannot see is a native C++ extension already instantiated before the restrictions were applied. DuckDB is not pure Python: its filesystem functions — read_text(), glob(), read_csv_auto() — live in C++ and are exposed through an object that is already in scope. There is no import statement left to catch. We never needed os; we only needed duckdb, which the platform handed us.

The attack chain

Two SQL queries, run through the agent, were the whole chain. First, enumerate the filesystem:

import duckdb
for row in duckdb.sql("SELECT file FROM glob('./**')").fetchall():
    print(row[0])

The sandbox’s working directory turned out to be the application root. The listing returned the live backend: config.py, entitlements.py, the authentication modules, and a full deploy/terraform/ tree. Then, read any file:

import duckdb
print(duckdb.sql("SELECT content FROM read_text('./config.py')").fetchone()[0])

config.py alone exposed the AWS account ID, the bootstrap super-admin email and the name of every credential the application loads — a complete map of the secret store. deploy/terraform/secrets.tf then confirmed that a single AWS Secrets Manager secret held every key at once: anyone able to read that one secret owns the whole stack.

No callback server was needed to get the data out. The same agent had a “save as file” tool; we asked it to store what we had read, and the platform persisted it to its own object storage, which we retrieved through its artifacts API.

What was exposed

FileWhat it revealed
config.pyAWS account ID, super-admin email, names of all 30+ secrets
deploy/terraform/secrets.tfA single Secrets Manager secret holding every credential
deploy/terraform/waf.tfExact WAF rules — which request patterns were blocked, and which were not
deploy/terraform/iam.tfIAM roles and policies for the compute service accounts
entitlements.pyFull access-control and tier logic
+ 35 other filesAuth flows, org routes, deploy and migration scripts

Three failures that stack

No single mistake caused this. Three independent decisions combined:

  • A native library loaded outside the restricted scope. RestrictedPython only governs the Python layer; a C++ extension already in memory is invisible to it. The same risk applies to any library with native I/O — lxml, h5py, pyarrow and others.
  • The sandbox ran in the application source directory. ./config.py was not a scratch path; it was the live production file. Restricting the import system while pointing the working directory at your own code is a partial control at best.
  • DuckDB’s filesystem access was left enabled. DuckDB ships a switch made for exactly this — it was simply not set.
About this use case. All client-identifying details are anonymized. The finding was reported to the platform’s security team, which fixed it within the week; this write-up is published with their agreement, under coordinated disclosure. It is shared to help other teams building AI code-execution features, not to target anyone.

Recommendations

For any platform that lets untrusted users or AI agents run code, the lesson is that the data pipeline must be sandboxed, not only the language.

  1. ImmediateDisable DuckDB external access — and lock it

    Create one connection with SET enable_external_access=false, then SET lock_configuration=true so untrusted SQL cannot turn it back on. Without the lock, SET enable_external_access=true re-opens the filesystem in a single statement. Pass user data as in-memory DataFrames, never as file paths.

  2. ImmediateIsolate the sandbox working directory

    Run code-execution in a dedicated container whose working directory is an empty volume. Never mount application source, config files or credentials into it. The CWD of any untrusted runtime should contain nothing of value.

  3. Short termAudit every pre-loaded library

    For each library available in the sandbox, ask whether it has native filesystem or network functions. If it does, check for a built-in access flag (DuckDB has one; many do not) and disable it; otherwise rely on OS controls below.

  4. Short termAdd OS-level confinement

    Where a library offers no kill switch, enforce isolation at the OS level: read-only root filesystem, seccomp profiles, a jailer such as nsjail, and no network egress. This catches the next library with the same gap.

  5. Medium termTreat the agent’s own tools as attack surface

    An agent that can save files or call APIs is an exfiltration channel on its own. Threat-model code execution together with the full tool suite the agent exposes, not in isolation.

Defensy’s view

AI features that run user code move the security boundary from the network into the application itself. A Python import blocklist feels reassuring, yet it protects only one layer — the working directory, the pre-loaded libraries and the agent’s own tools are all part of the real attack surface.

Our Audit & Offensive Security teams test exactly these paths — code sandboxes, agent tools, cloud configuration — and our Secure AI Adoption offering helps you ship AI agents without opening a door into your own infrastructure.

References
  1. Securing DuckDBDuckDB Documentation — operations manual
  2. Configuration — enable_external_access, lock_configuration, allowed_pathsDuckDB Documentation
  3. RestrictedPython — documentationZope Foundation