Flow
From open SQL box to safe reports
1Login first
Take the data routes off the skip-login list.
2Named reports
Each one runs a single fixed query.
3Narrow user
A database user that reads only safe views.
4Time limit
Set on the database side, not the app.
5Plain errors
An ID instead of the database message.
The first commit of my agent dashboard had an endpoint that runs any SQL you send it. It was called read-only. It wasn't, and it never asked who you were.
The app records what my coding agents do: each tool call, each session, and the full text of every prompt I type. That's a lot of my work in one database.
How the route worked
It took the SQL text, looked at the first word, and refused only if that word was a write like DELETE or DROP. Anything else ran on the app's main connection, and the rows came back along with the raw error text when something failed. The login check had a list of routes that skip login, and every database route was on it. The doc for the API says that was on purpose, so other agent tools could call it easily.
Ways past a first-word check
The first word tells you very little. A statement can start with WITH and still delete rows. A comment in front hides a DROP. EXPLAIN ANALYZE really runs the statement it explains. Two statements can probably share one request. And a query that just sleeps writes nothing, but it ties up a connection the rest of the app needs, and nothing set a time limit.
If the app connected as a superuser, which is what the fallback setting in the code used, a few skipped commands could even write files on the database server.
Reading was the real risk
Even a perfect check leaves plain reads, and plain reads return every prompt. I tend to paste logs and config into prompts, so I'd bet a secret or two is in there. Other open routes listed every table, and the raw errors filled in the gaps.
"Read-only" only answered one question. The ones that mattered were who's asking, what they can see, and how much a request can cost.
I suspect an agent wrote most of this, and I skimmed past it.
What I'd build instead
This is my plan, not something in the commit. Put the data routes behind login. Replace the SQL box with a few named reports that each run one fixed query. Give them a database user that can only read a few safe views, with a time limit the database enforces:
ALTER ROLE dash_reader SET statement_timeout = '5s';
Send back plain errors with an ID instead of the database's own words.