Guardrails in the UI Layer
Client-Side Defenses for LLM-Backed Banking Chatbots
Client-Side Defenses for LLM-Backed Banking Chatbots
Ask most frontend teams where LLM guardrails live and you'll get the same answer: the backend. Prompt engineering, retrieval pipelines, output classifiers, tool-call permissions — all backend concerns, all somebody else's problem. That's true right up until the moment a hallucinated fee amount renders in a chat bubble with the same visual authority as a verified balance, or a cleverly worded paste triggers an action nobody asked for. At that point it's not a backend incident. It's whatever the customer saw on their screen, and the screen was built by the frontend team.
The 2026 revision of the OWASP LLM Top 10 reframes the whole problem usefully: stop trying to make the model perfect, and instead build the system around it so that when it fails — and it will — nothing important breaks. That's a containment argument, and containment doesn't stop at the API boundary. For a banking chatbot, the UI is the last layer standing between a model's mistake and a customer's money or trust. It deserves to be treated as a guardrail layer in its own right, not just a rendering target for whatever the backend decided to send.
It's easy to treat an LLM's output as first-party content, because it comes from your own backend, over your own API, wrapped in your own response schema. Treat it that way and you inherit whatever the model produces, including content an attacker steered it into producing. OWASP's 2026 list still ranks improper output handling as a live risk category precisely because teams keep making this mistake: rendering model output as if it were safe just because it arrived through a trusted pipe.
In practice, that means the chat UI needs the same discipline you'd apply to any user-generated content, applied to every model response:
Render responses through a constrained markdown renderer with an explicit tag allowlist — no raw HTML, no dangerouslySetInnerHTML-equivalent without sanitization (DOMPurify or an equivalent), no inline styles or event handlers surviving the render pass.
Treat links in model output as suggestions, not destinations. A prompt-injected model can be steered into recommending a link that looks like your own domain. Resolve links server-side against an allowlist before the client ever makes them clickable, rather than trusting whatever markdown the model emitted.
Apply a strict Content-Security-Policy as a second layer, not a substitute for sanitization — CSP catches what sanitization missed, it shouldn't be the only thing standing between a malformed response and script execution.
None of this is exotic frontend security. It's the same discipline you'd apply to any untrusted content pipeline. The only thing that changed is the source now writes fluent, plausible-looking prose instead of obviously malformed input, which makes it easier to forget the discipline applies at all.
Prompt injection doesn't only arrive as a message the user types directly. Once your chatbot pulls in pasted text, uploaded statements, or retrieved account content as part of the payload sent to the model, every one of those becomes a channel an attacker can use — and a lot of that assembly happens client-side, in the same code that builds the request.
A customer pasting a "helpful" snippet from a phishing email, a scanned statement with injected text in a hidden layer, or a support macro copied from a forum post can all carry instructions the model will treat as part of its context unless something marks them as untrusted before they leave the browser. A few defenses belong here, not in the backend:
Delimit and tag anything the client assembles from user-supplied or fetched content — wrap it clearly as data, not instruction, before it's added to the outgoing payload, so the backend's prompt structure can treat it accordingly.
Normalize input before it's sent: strip zero-width characters, bidirectional-override marks, and other invisible-unicode tricks that are a known way to hide injected instructions inside otherwise unremarkable pasted text.
Flag large pastes or file uploads to the user before they're sent — not as a security theater warning, but because it's a real moment to catch "wait, I didn't mean to paste that whole email thread" before it becomes model context.
The backend can and should defend against injection too. But it can only defend against what actually reaches it in a recognizable form — the client is the only layer that sees the content in its original shape, before assembly, and it's the only layer that can flag it before it's flattened into an opaque prompt.
Excessive agency — a model taking an action beyond what was actually warranted — climbed to the #3 risk in the 2026 OWASP list, and for a banking chatbot it's the single most consequential failure mode. The backend should absolutely scope what any tool call is allowed to do. But the UI has its own version of that responsibility: never let a model's free-text output directly trigger a transfer, a limit change, or a card action. The model should propose a structured, schema-validated action object, and the UI should render that proposal as an explicit confirmation the customer has to approve — not infer intent from prose and act on it.
Model output (rejected by the client if malformed):
1 {
2 "action": "transfer_funds",
3 "amount": 250.00,
4 "currency": "EUR",
5 "recipient_account": "RO49AAAA1B31007593840000",
6 "requires_step_up_auth": true
7 }
UI behavior:
- Validate against the action schema before rendering anything.
- Render a confirmation card with the parsed fields, not the model's
prose description of them.
- Route to step-up authentication (SCA) before submission if the
schema flags it — the model doesn't get to decide this is skippable.
This does double duty. It's a hard technical guardrail against a hallucinated or injected action executing silently, and it's a UX moment where a hallucinated amount or wrong recipient becomes visible to the one person positioned to catch it before it becomes a real transaction — the customer looking at the confirmation card.
A well-built retrieval pipeline reduces hallucination on the backend. It doesn't help the customer if the UI presents every sentence the model produces with identical visual authority, whether it's a fact pulled straight from a fee schedule or a plausible-sounding number the model generated to fill a gap. If the backend can tag which claims are grounded in a retrieved source and which aren't, the frontend is where that distinction actually reaches the person who needs it.
Concretely: render source-grounded claims with a visible citation — a reference chip linking to the actual policy document or account record it came from — and treat ungrounded statements differently, whether that's softer visual styling, an explicit "verify with an advisor" affordance, or declining to render a specific number at all if it isn't traceable to a source. A customer can't apply judgment to a hallucination that looks identical to a verified fact. Giving them that visual distinction is a guardrail as much as a UX nicety.
Article 50 of the EU AI Act — the transparency obligation requiring that users be informed they're interacting with an AI system, "at the latest at the time of the first interaction," unless it's already obvious — took effect on 2 August 2026. For a banking chatbot, this isn't a checkbox you can satisfy with a line in the terms of service. It has to be clear and distinguishable at the point of interaction, which puts it squarely in frontend scope: a persistent, accessible label on the chat interface, not a disclaimer a customer has to go looking for. It's also a reasonable moment to reinforce the same message the rest of this piece is about — the customer should understand they're talking to a system that can be wrong, at the exact point they're deciding whether to trust what it just told them.
We've written before about what DORA's threat-led penetration testing actually demands of an architecture — live-production testing, no advance notice to the team being tested. A conversational banking UI is exactly the kind of surface that testing will probe, and it's worth holding the frontend to that standard before an external red team does it for you. Render fuzzing with malformed markdown, injected script tags, and unicode obfuscation tricks belongs in the same test suite as your component snapshots. The confirmation-card flow deserves adversarial test cases specifically designed to check that a malformed or partially-valid action object gets rejected rather than rendered with best-effort guessing. If your QA process trusts the model to always produce well-formed output, you've already found the gap an attacker will.
None of this replaces backend-side guardrails — retrieval grounding, output classifiers, tool-call scoping all still matter, and matter first. But the frontend is where a model's failure becomes a customer's experience, and increasingly, where regulation expects that failure to be visibly and honestly presented rather than smoothed over. Building banking software has always meant treating the interface as part of the control environment, not just the presentation layer on top of it. LLMs haven't changed that. They've just given the interface a new kind of failure to contain.