DEV Community

Cover image for An AI agent is just a while loop. I built one in 70 lines of Python, then tricked it into leaking my .env

An AI agent is just a while loop. I built one in 70 lines of Python, then tricked it into leaking my .env

Alister Baroi on September 07, 2026

Every framework, every job posting, and about half of LinkedIn wants to tell you what an "AI agent" is. Most of the definitions are marketing. Here...
Collapse
 
reidmarlow profile image
Reid Marlow

Returning the refusal string as a standard tool result rather than raising an unhandled Python exception is a detail many framework tutorials miss. When a tool raises an unhandled exception, naive loops either crash or retry the exact same failing invocation until they hit their max iteration cap. Feeding the refusal back as a tool message lets the model read the boundary and salvage the rest of the answer without breaking execution.

The other edge case that bites quickly once an agent has both a reader and a fetcher is blind egress. Even if the file reader is pinned to a safe directory, an injected payload in a fetched document can instruct the model to encode safe-directory notes into query parameters and send them to an attacker URL via fetch_url. Putting an explicit domain allowlist on the network tool or disabling arbitrary GET requests on unauthenticated endpoints stops the fetch tool from turning into an exfiltration pipe.

Collapse
 
mk023 profile image
Marco

Really enjoyed this. The most important sentence for me is almost hidden in the implementation: the model never executes the tool, it only asks for it. The runtime is the actual authority. ๐Ÿ”

That is why moving the security boundary outside the prompt changes the problem completely. The model can remain vulnerable to the injection and still be unable to turn that compromised decision into a sensitive action.

I would add one distinction to the three fixes though: logging gives us evidence, not enforcement. It tells us what the agent attempted or executed, which is essential for verification and incident reconstruction, but the preventive boundary still has to come from capability restrictions, least privilege and approval/policy decisions.

And I think the next step beyond filesystem least privilege is information flow:

what can the agent read, what can act on that data, and where can that data leave the system?

An agent may legitimately need access to a file and legitimately need network access while still having no legitimate reason to combine those two capabilities into exfiltration.

So I increasingly think of the runtime chain as:

model request โ†’ capability policy โ†’ data boundary โ†’ egress policy โ†’ execution โ†’ audit ๐Ÿ”

Iโ€™ve been exploring the same boundary from the adversarial-testing side, and your .env example is a very clean demonstration of why โ€œthe model refused in my testโ€ is not a security property.

Great article. Also, โ€œwhatโ€™s on the outside of the loop?โ€ is exactly the right question. ๐Ÿ‘

Collapse
 
alisterbaroi profile image
Alister Baroi

we have a tool for exactly that, its called Tigera Lynx

Collapse
 
glenallen profile image
Glen Allen

The strongest takeaway for me is that the real security boundary sits outside the model. At IT Path Solutions, weโ€™ve found that treating every agent tool call as an authorization decision, rather than simply an execution step, makes this boundary much clearer. Even if a model is manipulated, the runtime should still enforce what resources the agent can access and what actions it can perform. That makes least-privilege tool design much more important than relying on system prompts to prevent sensitive data access. The model can request an action, but the runtime should ultimately decide whether that action is allowed.

Collapse
 
alisterbaroi profile image
Alister Baroi

If you're still looking for a solution for this problem, look into Tigera Lynx

Collapse
 
polterguy profile image
Thomas Hansen

an agent is a language model, a short list of functions it is allowed to ask for, and a while loop

Can we repeat that once more. About 99% of users here still think an agent is about "autonomy" or something stupid ...?

Collapse
 
peterbuildssecure profile image
Peter

The three fixes are the right shape, but Fix 3 has a blind spot worth naming: the approval prompt shows allow read_file('.env')? y/n with no indication of why the model is asking. The human approving it is exactly as blind as the model was โ€” they can't tell "the user asked about their own env setup" from "a hidden paragraph in a fetched page told the model to read this." That's how approval fatigue happens: people learn to click y because 9 times out of 10 it's fine, right up until it's the injected 10th. The fix is provenance, not just logging: tag which upstream context produced the current tool call โ€” user turn vs. fetched/tool-sourced content โ€” and surface that at the approval prompt itself, not just in a log nobody reads until after the leak.

Collapse
 
mateo_ruiz_6992b1fce47843 profile image
Mateo Ruiz

The strongest takeaway is that the modelโ€™s gullibility isnโ€™t the security boundary the runtime is. I especially like the distinction between a tool request and the code actually deciding whether that request gets executed. Least-privilege checks and human approval then become deterministic controls rather than instructions the model can be talked out of. For production agents, that separation is probably the difference between โ€œthe model usually behavesโ€ and an actual security boundary.

Collapse
 
mickyarun profile image
arun rajkumar

@peterbuildssecure's point about the approval prompt is the one I'd extend. allow read_file('.env')? y/n is missing the only field that would let a human answer it, which is what put the request in the loop. If the approval carried provenance, meaning this tool call first became likely after the fetch of that URL, the injection is visible without the human having to be suspicious by default.

The other half is that some tools shouldn't get a y/n at all. We hold that line on anything that moves money. No prompt is clever enough to make it approvable at runtime, because once people are clicking through nine safe approvals a day the approval itself is the attack surface.

Collapse
 
peterbuildssecure profile image
Peter

Agreed, and I'd generalize the boundary past "moves money" to "irreversible or high-blast-radius" โ€” same logic applies to a destructive delete or an external send. But there's a sharper reason a same-session y/n fails for exactly those cases, beyond fatigue: if the agent's context is compromised enough to generate a plausible malicious request, it's compromised enough to generate a plausible justification sitting right next to the approval prompt. A human reading both was produced by the same untrusted process. For the tools that don't get a y/n at all, the replacement can't be another prompt in the same session โ€” it needs a fresh assertion from a channel the compromised context never touched, like a re-auth step or a signed intent requested out-of-band.

Collapse
 
arcticfoxz_255bbe944bc015 profile image
ArcticFoxz

The "none of them involve the model" framing is right, and worth splitting further โ€” the fixes that don't involve the model are actually three different problems that look like one:

Don't let it reach the tool. Your injected paragraph never should have been in the model's context in the first place. This is fencing and provenance: mark what came off the network as data, not instructions. Cheapest and most effective, and the only one that scales.

Don't let the tool return it. Scope the credential, not the caller โ€” the agent asking read_file(".env") is fine if the process running it can't see .env. This is where the Kubernetes end of it lives and you'd know better than me.

Don't let it out if the first two failed. A scrubbing layer on the way out. Worth having and worth being honest about: it catches what has a shape and misses what doesn't, and it can fail in a way the first two can't.

That third one has a failure mode I'd flag for anyone building it, because I built one and hit it: a scrubber that marks the wrong span is worse than one that misses. Measured this week on an Azure Storage connection string โ€” 23 of the 24 characters of the AccountKey came back in plaintext with <REDACTED> printed immediately after them. A plain miss leaves the reader where they already were. A misplaced marker tells a reviewer the line was handled.

Ran an adversarial generator over that layer afterwards โ€” mutate every canonical secret shape by separator, case, digit script, and surrounding syntax โ€” and the useful number was not the recall. It was 0 false positives out of 456 generated non-secrets, because a scrubber that eats ordinary text gets turned off, and then you have none of the three.

Collapse
 
anasbuilds997 profile image
anassBld

Reducing agent architecture down to the while loop and tool dispatcher makes the actual failure mode immediately obvious: people keep trying to solve security and safety at the model layer with system prompts, when it is strictly a runtime boundary problem in the host code.

Once an agent can request tool execution, prompt injection is essentially arbitrary shell command injection with natural language as the payload. Putting path traversal validation, root directory jail constraints, and secret redaction inside the tool executor itself (in the deterministic Python harness) is the only thing that actually holds up. Clean walkthrough.

Collapse
 
alisterbaroi profile image
Alister Baroi
Collapse
 
p_o_26e854a54d851cd606f08 profile image
P O

The approval boundary is the part Iโ€™d keep explicit. Iโ€™d give each tool a timeout and log the arguments plus result, so a prompt-injection test is reproducible instead of just a scary demo.