We hired a security engineer and got back 123 findings

buffer_overlord1 pts0 comments

We hired a security engineer and got back 123 findings — Chovy's Blog

We hired a security engineer and got back 123 findings

2026-08-16, by Anthony “chovy” Ettinger.

How this was written: drafted with an AI assistant from the advisory<br>records, the pull requests, and my own notes taken while doing the remediation. Every count in this<br>post came out of the GitHub advisory API and the Postgres catalogs, not out of memory. The<br>advisories themselves are private to the repos, so you'll have to take the numbers on<br>faith.

The setup

Eduardo Camarillo joined Profullstack this month as our first dedicated security engineer. I gave<br>them two repositories and no scope restrictions: CoinPay Portal , our self-hosted<br>crypto payment processor, and QryptChat , our post-quantum encrypted messenger. Both are<br>live. Both handle exactly the kind of thing you do not want handled badly — one moves money,<br>the other is supposed to be the app where the contents are nobody's business but yours.

I expected a report. What arrived over about seventy-two hours was a filing cabinet.

The count

Across the two repositories, 123 confirmed findings , tracked as 63 individual<br>GitHub security advisories:

CoinPay Portal — 86 confirmed findings: 3 critical, 39 high, 29 medium,<br>15 low. The critical and high ones were filed individually; the mediums and lows arrived as a single<br>umbrella &ldquo;remediation list&rdquo; document. Three more critical/high reports landed<br>afterward, separately.

QryptChat — 37 vulnerabilities plus 37 architectural deficiencies,<br>filed as 17 advisories: 2 critical, 11 high, 4 medium.

As of today all 63 advisories are published, which on GitHub is the terminal state<br>for a fixed advisory — there is no separate &ldquo;closed.&rdquo; The fixes went out across<br>roughly a dozen pull requests and about ten database migrations applied to production.

I want to be honest about the shape of that number, because &ldquo;123 vulnerabilities&rdquo; is<br>the kind of headline that is usually doing marketing work. A meaningful fraction of these were<br>latent — real defects in the code or the schema, genuinely exploitable under a<br>plausible near-future configuration, but not reachable in production as it was deployed that<br>morning. More on why that distinction nearly tripped me up in a minute. A handful of others were<br>duplicates of one root cause across a family of call sites. What is not negotiable is the other<br>end: some of these were live, unauthenticated, and bad.

The five that made my stomach drop

Ranked by how long I stared at the screen after reading the title.

Unauthenticated remote data destruction. QryptChat had a<br>SECURITY DEFINER Postgres function named delete_encrypted_data_only,<br>callable by the anon role. Any person on the internet with the public API key —<br>which ships in the browser bundle, because that is what a public anon key is — could<br>invoke it. That is not a vulnerability so much as an unlabeled button wired to the building's<br>demolition charges. It was one of 48 SECURITY DEFINER functions in the public schema<br>reachable by anon. All 48 are now unreachable, verified by querying<br>has_function_privilege over pg_proc rather than by trusting the<br>migration.

Every user's phone number, readable by every user. QryptChat's row-level<br>security policy on the users table was USING (true). Any authenticated account could<br>read every row: phone numbers, identifiers, the lot. It is now own-row OR<br>shares_conversation_with(id). The busiest account on the platform went from being able<br>to see all 86 users to seeing 14.

Unauthenticated identity spoofing. A sync_user_with_auth RPC<br>accepted a phone number and overwrote the record. No authentication.

Hardcoded service-role key fallbacks. On CoinPay Portal, the specific literal<br>Eduardo cited had already been removed — but the pattern hadn't. Sixteen other files fell<br>back to the anon key or an empty string when the service-role key was missing. Falling back to anon<br>does not throw. It silently downgrades, RLS begins applying to code written on the assumption that<br>it does not, and the symptom surfaces three layers away from the cause a week later. Every one of<br>them now routes through a client that refuses to construct without a real key.

13,711 rows of payment amounts readable by anon. A CoinPay Portal<br>reputation-receipts table. This one is interesting specifically because almost all of its<br>neighbours in the report were not live, and I only know which was which because I stopped<br>reading policies and started assuming roles. Which brings me to the actual point of this post.

Four things I learned that generalize

The findings themselves are ours. These four are yours too, if you run Postgres behind an app.

1. Column-level REVOKE is a no-op against a table-level grant

Supabase grants table-level SELECT to anon and<br>authenticated. If you then write REVOKE SELECT (secret_column) ON users FROM<br>authenticated, you have subtracted nothing, because the table-level grant already permits<br>every...

security anon findings back because level

Related Articles