I added DuckDB to my SQL playground, and it turned out to be a sandboxing problem

I added DuckDB to my SQL playground, and it turned out to be a sandboxing problem

1 4 14
calendar_today agoschedule4 min read

I added DuckDB to my SQL playground, and it turned out to be a sandboxing problem

Sharing a small build-in-public update: sqlize.online, a free tool for testing SQL queries, checking JOINs, and sharing reproducible examples, just got DuckDB support alongside the ~30 engines it already had (MySQL, Postgres, SQL Server, Oracle, Firebird, ClickHouse, SQLite...).

I'd wanted to add DuckDB for a while — I use it a lot in my own projects and like having local OLAP around. The holdup was boring: the whole thing runs on PHP, and PHP's PDO talks to MySQL, Postgres, and SQLite out of the box, but had nothing for DuckDB.

That changed when I found pdo_duckdb by Ilia Alshanetsky — a real PDO driver for it. That's the piece that made this worth doing.

Why DuckDB specifically

Short version: it's "SQLite for analytics." Vectorized engine, built for heavy aggregations on a single core, not for transactions.

What actually got me was that it reads Parquet on the fly, from disk or over HTTP, with no separate load step. For a playground, that's great — instead of a two-row toy table, you can hand people something real. So I preloaded NYC's Yellow Taxi trip data for January 2024, about three million rows, sitting in a table called yellow_tripdata:

SELECT
    PULocationID,
    COUNT(*) AS total_trips,
    ROUND(AVG(total_amount), 2) AS avg_fare
FROM yellow_tripdata
GROUP BY PULocationID
ORDER BY total_trips DESC
LIMIT 10;

That's a table, not a file path — more on why that matters below.

Building the driver, plus an unrelated Debian surprise

pdo_duckdb needs PHP 8.1+. My setup runs several PHP versions side by side for different engines, so that already cuts out a couple of pools. Worse, the latest release only ships prebuilt binaries for PHP 8.4/8.5 — on 8.1, where this feature currently lives, I had to build from source: phpize && ./configure --with-pdo-duckdb=... && make install, linked against libduckdb.so. Not exotic, but not a one-command install either — about half the engines in this project get built by hand at this point.

While I was at it I hit a completely unrelated bug: bumped that PHP pool to the latest patch release, and the image build broke for no obvious reason. Turned out the base image had quietly moved from Debian bookworm to trixie, which renamed libaio1 to libaio1t64. Rolled the base image back. Cost me about forty minutes of confusion for something that had nothing to do with DuckDB at all.

The actual problem: DuckDB runs inside the PHP process

MySQL and Postgres live in their own containers and can't see the PHP worker's filesystem at all. DuckDB is embedded — it opens up right inside PHP-FPM. Good for speed, no sockets or IPC in the way. Bad because by default it can reach anything the process can reach: COPY ... TO '/etc/whatever', read_csv from any path, read_parquet from any URL, INSTALL for arbitrary extensions.

Not something you want available to arbitrary user SQL.

First attempt was the obvious one — set it in SQL directly:

SET enable_external_access = false;
SET allow_community_extensions = false;
SET lock_configuration = true;

This genuinely works, and lock_configuration = true really does stop the same session from undoing it later. But the driver has its own separate protection: it flatly refuses a chunk of security-sensitive settings — paths, extension autoload — when they're passed via the DSN or PDO::DUCKDB_ATTR_CONFIG at connect time. On purpose, so the app using the driver can't accidentally weaken its own config.

The real mechanism turned out to be PHP's own open_basedir. Set it — even at runtime, with ini_set() — and the driver shuts off DuckDB's entire external-access surface: read_csv, read_parquet, COPY, ATTACH, httpfs, INSTALL, all of it, no matter which path gets requested. Regular CREATE TABLE/INSERT/SELECT against the session's own file keep working fine.

ini_set('open_basedir', '/code:/tmp/sessions:/tmp/databases');
$pdo = new PDO("duckdb:{$sessionFile}");

I tried stacking both — open_basedir plus the SET statements on top, just for extra safety. Didn't work: once open_basedir is active, the driver rejects allow_community_extensions outright ("not allowed when open_basedir is set") and the connection breaks. Had to pick one. Kept open_basedir — it covers everything on its own.

And yes, there was a bug. I was applying open_basedir right before running a query, but not before handing a raw PDO connection to user-written PHP code — which happens when someone runs PHP and SQL together and picks DuckDB as the engine. So: submit a PHP script with no SQL in the separate field, get back a completely unrestricted $pdo, and $pdo->exec("COPY (SELECT 1) TO '/tmp/whatever.csv'") just works. Caught it myself before it shipped anywhere public.

Why the dataset isn't loaded on every request

Three million rows over HTTPS on every session would be brutal — a cold build takes anywhere from 5 to 35 seconds. So it's built once at deploy time, by a separate script using a plain connection with no open_basedir set (downloading the file there is a legitimate job for that script, not for user SQL), and cached to a shared file. Every session after that is just a copy() — a fraction of a second.

Try to break it

If you find a way to read a server file or get around open_basedir, I'd rather hear about it directly than see the service go down. No bug bounty budget, but I'll credit you publicly if you find something real.

Try it here: sqlize.online — pick "DuckDB" or "DuckDB Preloaded" as the SQL version. Happy to answer questions about how any of this is built.

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

Delivering Database Changes

Steve Fentonverified - Jul 22

5 Things This Playwright SQL Fixture Does So You Don't Have To

vitalicset - Apr 13

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

The Hidden Program Behind Every SQL Statement

lovestaco - Apr 11

Comparison: Universal Import vs. Plaid/Yodlee

Pocket Portfolio - Mar 12
chevron_left
930 Points19 Badges
Haifa, Israelsqltest.online
4Posts
1Comments
2Connections
I'm Slava Rozhnev, the creator and primary developer behind sqltest.online (interactive SQL practice... Show more

Related Jobs

View all jobs →

Commenters (This Week)

4 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!