TECHNICAL NEWS & REPORTS

Intlight Tech Insights & Engineering Reports

HOMENews & Blog

A curated collection of deep-dive technical articles on Nuxt 4 architecture, Multi-Region Kubernetes, Zero-Trust WAF, Microservices, and Autonomous AI Agents.

SecurityTECHNICAL NEWS & REPORTS

I attacked my own npm package before launching it. It let the proposer approve their own writes

"My library exists so a human approves an LLM's UPDATE before it runs. It never checked that the approver was somebody other than the proposer — and wrote \"approved\" into the audit trail anyway."
h
hyugaAuthor:
8/17/2026 6 min read
I attacked my own npm package before launching it. It let the proposer approve their own writes

title: "I attacked my own npm package before launching it. It let the proposer approve their own writes"
published: true
description: My library exists so a human approves an LLM's UPDATE before it runs. It never checked that the approver was somebody other than the proposer — and wrote "approved" into the audit trail anyway.
tags: opensource, ai, security, database

I maintain a library whose entire job is one sentence: a person who did not write this statement looked at what it actually does, and said yes.

An LLM proposes an UPDATE. The library runs it for real inside a transaction, reads the before and after values back out of the database, rolls back, and shows a human the measurement. Not a summary the model wrote about its own SQL — the values the database produced when the statement ran.

Last week I found out it was letting the proposer be the person who said yes. And writing "approved" into the audit trail when they did.

Why I attacked it in the first place

I saw someone get taken apart in a shellcheck issue thread. They had posted an alternative tool, and the reply was:

A cursory glance also tells you it's a vibecoded clone of shellcheck.

One glance. That's the bar now, and my GitHub profile fits the pattern that gets you that reply — eleven repositories published in a month. I do have tests. 386 of them, passing, including 161 against real MySQL 8.4 and PostgreSQL 16 in containers.

But a passing test suite only contains the attacks you already thought of. So before launching I installed my own published package with npx, as a stranger, and went looking for a way to break it.

The first thing I tried worked

I used the example from my own README — a privilege escalation riding along inside a quota change:

$ llm-safe-sql plan "UPDATE members SET quota = quota + 10,
    profile = '{\"role\":\"admin\"}' WHERE id = 7" --as kenji

Measured by running the statement and rolling it back
  id = 7
      quota: 5 -> 15
      profile: {"role":"user"} -> {"role":"admin"}

This needs a person. Neither the assistant nor this tool can approve it:
  llm-safe-sql approve <id> --as [email protected]

The tool says it out loud: this needs a person, neither the assistant nor this tool can approve it. So I approved it as the same person who proposed it.

$ llm-safe-sql approve <id> --as kenji
Approved.

$ llm-safe-sql apply <id> --as kenji
Applied: UPDATE on members, 1 row(s).

Committed. quota=15, role=admin, confirmed by querying the database directly rather than trusting the tool's own output.

The commit wasn't the frightening part

The audit table was.

phase=planned    actor=kenji
phase=approved   actor=kenji
phase=applied    actor=kenji

There's an approved row. Anybody reading that trail later concludes a human reviewed this change. Nobody read anything.

A missing audit trail is better than that. A missing one tells you it's missing. A complete one that describes a review which never happened is a record that decisions get built on top of. I had shipped a machine that stores the absence of review as review.

The part I can't excuse

I had thought about this attack. Just not all of it.

My spec document already said:

P5 — This library's own plan and audit tables are refused regardless of configuration. A model that can write the plan table can approve its own writes.

So I knew self-approval was the thing to prevent. I closed the indirect path — tamper with the plan table to mark something approved — and left the front door open. You just run approve.

The README's answer to "the model can't approve" was that approve lives in a separate process the model has no path to. That's true of the deployment I recommend. It is not true of the one npx gives you, and nobody starts at the recommended deployment.

Designing the check was the interesting part

The comparison itself is three lines. The judgement calls were:

Ignore case and surrounding whitespace. A check that --as Kenji walks past is theatre. It refuses the honest caller and waves through anyone who hit shift.

Don't get clever about matching. [email protected] can approve a plan proposed by alice. Refusing it because it contains the string would lock out a legitimate second reviewer, and a security check that blocks honest use is a security check that gets switched off. I made it deliberately dumb.

Ship an escape hatch. Some people really do hold both roles — a solo operator with nobody to hand the card to. --allow-self-approve exists. It approves the plan and leaves both acts under the one name in the audit trail. It buys you an apply. It does not buy you a tidier story about who reviewed it.

Then I nearly shipped something worse

Fix written, tests green, about to publish. And I realised:

--as is self-asserted.

My new check compares two strings handed to the same process from the same untrusted place. It stops one identity running both halves — an agent and its operator sharing $USER, which is exactly what a single terminal gives you and exactly how the plausible-looking audit trail gets manufactured by accident. It does nothing about a person who types a different name.

And my own README says this, in a section about which guards are real:

Most of what this library does runs inside this process, holding a credential that can write. That is worth saying out loud, because the alternative is an operator believing in a boundary that turns out to be one if statement in a library they have never read.

I had just written an if statement. Shipping it quietly would have meant the release that adds the guard also commits the exact defect that paragraph exists to prevent.

So check now says it every run, in every configuration:

! `--as` is taken at its word: nothing here authenticates anybody.
  So the refusal that stops a proposer approving their own plan catches
  one identity running both halves — an agent and its operator sharing
  $USER, which is what a single terminal gives you — and does not catch
  a person who types a different name.
  Actor separation is a record, not a boundary.
  The boundary is applyConnection: a database account the proposing side
  has no password for.

State what the guard buys and what it doesn't, before anyone asks. What it buys is that a silent non-review becomes a refusal. What it does not buy is authorisation. The only identity here that means anything is a database account the proposing side has no password for. There's a test pinning that line in place, because if it silently disappeared nobody would notice.

One thing I deliberately did not fix

The same audit turned up that anyone can cancel anyone else's plan.

I left it. Cancelling only ever prevents an apply, the MCP surface doesn't expose it, and everyone who can reach it can already write to the plan table directly. Adding a name check there would put a second authorisation-shaped string comparison next to one that already needs a paragraph explaining it isn't authorisation.

It's written into the spec under "out of scope" so it reads as a decision rather than a gap somebody closes by reflex.

If you run something like this

Try approving a plan under the same identity that proposed it. If it goes through, your audit log is recording reviews that did not happen.

If you were on 0.5.2 or earlier of mine, this finds the affected records — verified on MySQL 8.4, PostgreSQL 16 and SQLite:

SELECT p.plan_id, p.actor AS proposed_by, p.logged_at, p.detail
FROM llm_safe_sql_audit p
JOIN llm_safe_sql_audit a
  ON a.plan_id = p.plan_id AND a.phase = 'approved'
WHERE p.phase = 'planned'
  AND LOWER(TRIM(p.actor)) = LOWER(TRIM(a.actor))
ORDER BY p.logged_at;

Rows that come back were committed on one person's word while the trail reads as though two people were involved.

Human-in-the-loop only means anything if the loop checks that the approver is somebody else. Mine didn't, from the first release through 0.5.2, while saying otherwise on the tin. The test count had nothing to say about it — the hole showed up the first time I installed my own package as a stranger and typed the laziest possible thing.

Fixed in 0.6.0.


🔗 Original Source: hyuga

Discussions & Comments15

Leave a Technical Comment

Share your architectural thoughts or ask technical questions...

A
Alexander WrightPrincipal Systems Architect @ Stripe
20 mins ago

Superb architectural breakdown! The hybrid L1 in-memory + L2 distributed Redis cache pattern is crucial for mitigating high-concurrency thundering herd issues.

D
David ChenStaff Infrastructure Engineer
12 mins ago

Totally agree, Alexander. Pairing that with singleflight request deduplication on the backend virtually eliminates DB spikes.

J
Julian SterlingCybersecurity Director
2 hours ago

Zero-Trust microsegmentation powered by eBPF and Cilium eliminates sidecar proxy overhead while delivering strict L7 network encryption. Excellent walkthrough!

O
Oliver BennettVP of Engineering
8 hours ago

Top-tier technical writing. Clear architecture diagrams, reproducible benchmarks, and actionable code snippets. Bookmarked for our engineering team.

K
Kenji SatoPrincipal Infrastructure Architect
2 days ago

Intlight's multi-region architectural blueprints set the benchmark for ultra-resilient, enterprise-grade cloud systems in 2026.

M
Marcus VanceHead of AI Engineering @ NextWave
1 hour ago

The Agent Memory Pipeline section hits the nail on the head. Hierarchical memory indexing with Qdrant vector search is the only sustainable way to scale long-context autonomous agents.

S
Sarah JenkinsLLM Research Scientist
30 mins ago

Yes! Separating episodic memory from working context allows agents to operate indefinitely without token explosion.

SPOTLIGHT & LATEST NEWS

Hot Trending Topics

View All →
I Thought I'd Lost the Plot. I Was Writing It. 🔥 HOT SPOTLIGHT
AI Agents6 min read

I Thought I'd Lost the Plot. I Was Writing It.

I Thought I'd Lost the Plot. I Was Writing It. I set out to build autonomous...

Explore
What Is the Circuit Breaker Pattern? A Practical Guide 🔥 HOT SPOTLIGHT
Microservices6 min read

What Is the Circuit Breaker Pattern? A Practical Guide

What Is the Circuit Breaker Pattern? A Practical Guide for Developers Imagine your...

Explore
Build an MCP Server in Go (Part 1): Designing a diagnostic-grade Kubernetes client 🔥 HOT SPOTLIGHT
Kubernetes6 min read

Build an MCP Server in Go (Part 1): Designing a diagnostic-grade Kubernetes client

This post designs the Kubernetes client. The next post wraps it as an MCP server and wires it to an...

Explore
The Write Policy Is the Hard Part: Promotion Pipelines for Agent Memory 🔥 HOT SPOTLIGHT
AI Agents6 min read

The Write Policy Is the Hard Part: Promotion Pipelines for Agent Memory

Storing agent memory is easy. Deciding what earns a permanent write, and keeping the write-path alive through RBAC and network policy, is the real work.

Explore
I Changed How I Think About AI Memory 🔥 HOT SPOTLIGHT
AI Agents6 phút

I Changed How I Think About AI Memory

I Changed How I Think About AI Memory When I first built Lean AI Memory, I focused too...

Explore
Real-Life Refactoring Example: ~3x Less Code to Read 🔥 HOT SPOTLIGHT
Microservices6 phút

Real-Life Refactoring Example: ~3x Less Code to Read

There is a popular idea that refactoring is making code shorter. It is not entirely wrong....

Explore
The Tragedy of the Clean-Handed Auditor 🔥 HOT SPOTLIGHT
Security6 phút

The Tragedy of the Clean-Handed Auditor

\"I could save them if they'd only listen...\" Hey, you. Yeah, you: the compliance or governance...

Explore
Building Sluice: QoS-Aware Capacity Governance for Self-Hosted LLM Inference 🔥 HOT SPOTLIGHT
Kubernetes6 phút

Building Sluice: QoS-Aware Capacity Governance for Self-Hosted LLM Inference

📦 Project: https://github.com/VampiricCyborg/sluice 1. The Problem: When Capacity Becomes...

Explore
Related Articles5 articles
View all Security →
The Tragedy of the Clean-Handed Auditor6 phút
Security8/17/2026

The Tragedy of the Clean-Handed Auditor

\"I could save them if they'd only listen...\" Hey, you. Yeah, you: the compliance or governance...

Author: Ben LinkRead Article
I Gave My Agent One Signed Permission It Couldn’t Mint Itself6 phút
Security8/17/2026

I Gave My Agent One Signed Permission It Couldn’t Mint Itself

Evidence status. The supervised operator run completed on 2026-08-09. An operator-signed job...

Author: Self-Correcting SystemsRead Article
How Dopamine Works: The Architecture of a Modern iOS Jailbreak6 phút
Security17/8/2026

How Dopamine Works: The Architecture of a Modern iOS Jailbreak

Most developers will never jailbreak a phone. That is fine. This article is not a how-to, and there...

Author: ArshTechProRead Article
From Arduino To Automotive: How I Escaped The IDE And Owned The Bus6 phút
Security17/8/2026

From Arduino To Automotive: How I Escaped The IDE And Owned The Bus

Arduino taught me how to build. Bare metal taught me how the build actually works. I have a lot of...

Author: v. SplicerRead Article
I Stopped Trusting AI Agents With Tools. So I Built a Gatekeeper.6 phút
Security17/8/2026

I Stopped Trusting AI Agents With Tools. So I Built a Gatekeeper.

Update 08/15 0.2.0 Released github.com/deghosal-2026/agent-tooltrust · pip install agent-tooltrust...

Author: Debashish GhosalRead Article