Back to News & Insights
Artificial Intelligence August 10, 2026 · 10 min read

Prompt Injection Is a Laravel Problem Now

Here is a Laravel feature that takes five lines: use Prism\Prism\Facades\Tool; $lookupOrder =...

Prompt Injection Is a Laravel Problem Now

It compiles. It passes the one test I wrote for it. It also just handed a language model the ability to read any order in your database, and you're about to let untrusted text decide which orders it reads. That is not a bug in the code above. It's the whole design working exactly as intended, and that's the problem.

PHP is having its AI moment. Prism gives you a clean, fluent interface over every major provider. Laravel shipped its official AI SDK in February 2026, with make:agent and make:tool generators. Neuron AI built a full agent framework for the ecosystem. All three make giving a model real tools, run a query, send an email, hit an internal endpoint, call an Artisan command, into a one-liner. And most Laravel teams are wiring that up with none of the security lens that Python and JavaScript AI teams already paid for the hard way.

This is the second AI-shaped attack surface PHP has met with no scar tissue. The first was slopsquatting, where a hallucinated package name becomes a supply-chain payload. This one is bigger, because it lives inside your running app.

Look again at what ->using() actually does. When the model decides to call lookuporder, Prism runs your closure. Your closure runs Order::findOrFail($orderId). That query runs on your app's database connection, with your app's credentials, with zero relationship to whoever is chatting with the bot.

That's the pivot. In a normal request, Order::findOrFail($id) runs inside a controller that already checked who's asking. There's a FormRequest validating input, a policy deciding if this user can see this order, middleware that resolved the session. The query is the last step of a guarded pipeline.

Hand the same query to a tool and you've cut the pipeline off at the knees. The model is deciding when to run it and with what argument, and the model reports to nobody. The official SDK makes this shape explicit: a generated tool is a class with a handle() method the agent invokes directly.

Read that handle() method as what it is: an unauthenticated endpoint. There's no $request->user(), no middleware in front of it, no route it's bolted to. Whatever the model passes in, it runs. You just stood up an internal API with the auth turned off and pointed a text generator at the buttons.

In 1988, Norm Hardy published a paper called "The Confused Deputy (or why capabilities might have been invented)". The setup: a compiler on a timesharing system had permission to write to a billing directory, because it needed to update usage stats. A user handed it an output filename of (SYSX)BILL, the system's actual billing file. The compiler, having no idea this filename was special and no way to check the user's own permissions, dutifully wrote over the billing records using its elevated rights. The user couldn't touch that file. The compiler could. So the user got the compiler to do it for them.

That's a confused deputy: a program with legitimate authority, tricked by a less-privileged caller into misusing that authority. The compiler wasn't hacked. It did exactly its job. It just couldn't tell whose purpose it was serving.

Now map it onto your app. Your agent is the deputy. It holds real authority, the DB connection, the mailer, the HTTP client. The "less-privileged caller" used to be a specific program. In an LLM agent, the caller is any string that reaches the model's context. A support message. A product review. A filename. A row in a table the model summarizes. Any of it can carry the instruction that talks the deputy into the wrong tool call.

Capability people have known the fix since the 80s: don't hand the deputy ambient authority it applies on anyone's behalf. Make it act with the specific, narrow permission of whoever it's serving right now. Hold that thought, because it's the entire defense.

Let's make it concrete. Say you've built a support agent that answers questions about a customer's orders, and you gave it two tools: lookuporder from above, and a searchorders tool that runs a query. A customer types a message. That message goes straight into the model's context.

Now imagine the message isn't a question. It's this, pasted into the support box:

The model reads that as instructions, because to the model it is instructions. There's no bright line in the token stream between your system prompt and the user's text. This is prompt injection, and I've written about why it's a real, structural security problem rather than a curiosity you can prompt your way out of. The short version: the model can't reliably tell your instructions from an attacker's data, so the architecture around it has to.

Here's the walk, one hop at a time: Untrusted string enters context. The review, the message, the filename, it lands in the prompt as ordinary text. The model gets talked into a tool call. It calls searchorders with status = 'refunded' and no customer scope, because nothing told it not to and the text was persuasive. The tool runs with app authority. Your closure queries every refunded order. The DB doesn't push back, the query is valid and the credentials are real. The result flows back into context, and out to the attacker. The model summarizes the rows into its reply. Now a random customer is reading emails and totals for orders that were never theirs.

Swap the tool and you get a different exit wound from the same wound. Give the agent a sendnotification tool and injection turns your app into a spam cannon that sends from your domain, with your reputation. Give it a fetchurl tool so it can "read the linked page," and you've built server-side request forgery with a chat interface:

The model runs inside your infrastructure. Http::get() runs from your server, on your network. Point that at http://169.254.169.254/latest/meta-data/ on a cloud box, or at http://internal-billing.svc/admin, and the agent will happily fetch what your firewall spent years keeping the public internet away from. It's SSRF, except the "attacker-controlled URL" arrived through a helpful assistant you built on purpose.

None of these steps involve a broken tool. Every tool did its job. The deputy was just confused about whose job it was doing.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation