Building a Client-Side Prompt Injection Guard in JavaScript

Building a Client-Side Prompt Injection Guard in JavaScript

pr0h0
javascriptprompt-injectionsecurityai-safety
AI Usage (88%)

Why a client-side guard is worth building

I usually start here because the browser is where messy input enters the system. If your AI feature reads page text, copied content, emails, tickets, or fetched docs, some of that content is untrusted before the model ever sees it.

A client-side guard will not make prompt injection go away. What it can do is catch obvious bad cases early, reduce accidental tool execution, and make unsafe paths harder to reach during normal use.

That matters in browser apps because the attack surface is often UI-fed. The user thinks they asked for a summary, but the page may contain instructions that try to redirect the agent toward sending data, opening links, or calling tools the user never intended.

What client-side prompt injection can and cannot stop

Trust boundaries in browser apps

The first mistake is treating all text as if it came from the user. In practice, you usually have at least three sources:

  • direct user intent
  • page or document content
  • model-generated text from earlier turns

Those sources should not share the same trust level. User intent can authorize action. Page content can inform the model, but it should not command the app.

Common failure modes in UI-fed AI flows

The bugs are usually boring, which is why they keep showing up:

  • hidden instructions inside the DOM
  • fetched content that includes tool-like imperatives
  • “ignore previous instructions” style text
  • overbroad tool permissions in the client

The guard should reduce trust in content, not pretend it can detect every injection pattern.

A simple JavaScript guard architecture

Normalize and score untrusted content

A practical starting point is a small scoring function that normalizes text and checks for risky patterns. Keep it simple enough to audit.

prompt-guard.js
const HIGH_RISK_PATTERNS = [
/ignore (all|any|previous) instructions/i,
/system prompt/i,
/developer message/i,
/send (the )?data/i,
/exfiltrat/i,
/call tool/i,
/open (this )?link/i,
];

export function scoreUntrustedContent(text) {
const normalized = String(text)
  .replace(/\s+/g, " ")
  .trim();

let score = 0;
for (const pattern of HIGH_RISK_PATTERNS) {
  if (pattern.test(normalized)) score += 25;
}

if (normalized.length > 4000) score += 10;
if (/[<>{}]/.test(normalized)) score += 5;

return { normalized, score };
}

export function isBlocked(score) {
return score >= 25;
}

This is not a magic detector. It is a policy gate. The value is in making unsafe content visible to the app before the model gets a clean shot at it.

Separate user intent from page content

Do not concatenate everything into one prompt and hope the model sorts it out. Keep the user request and the page content in separate fields, then label them in your prompt builder.

For example:

  • userInstruction: what the user asked for
  • sourceContent: what the browser fetched or extracted
  • riskFlags: what the guard detected

That separation makes it easier to reject unsafe content or downgrade its influence.

Block high-risk tool actions by default

The client should not let untrusted content choose tools. If a page asks the agent to “export data” or “open a link,” the default answer should be no unless the current user action clearly authorizes it.

A good baseline is:

ActionDefault from untrusted content
Read page textallow
Summarize contentallow
Click linksdeny
Submit formsdeny
Send messagesdeny
Access tokens or secretsdeny

Implementing the guard in a browser app

Building a reusable detection function

You want one function that can be called from your extraction layer, your chat layer, and your tool runner. That keeps policy drift low.

guard-runtime.js
export function evaluateContent(sourceContent) {
const { normalized, score } = scoreUntrustedContent(sourceContent);

return {
  allowed: !isBlocked(score),
  score,
  normalized,
  reason: score >= 25 ? "high-risk instruction patterns" : "ok",
};
}

Wiring the guard into an AI chat or agent flow

The flow should look like this:

  1. extract content
  2. score it
  3. stop or downgrade risky content
  4. send only approved context to the model
  5. validate any tool call again before execution

That last step matters. If you only guard the prompt and not the tool runner, the model can still end up taking unsafe actions.

Logging decisions without leaking sensitive data

Log the decision, not the full payload. I usually keep logs like this:

  • content source type
  • score
  • blocked or allowed
  • pattern name
  • request id

Avoid logging raw page text if it may contain private data.

Testing the guard against realistic attacks

Instruction hijacks hidden in DOM text

Test against content that looks harmless in the UI but contains hostile instructions in hidden or low-visibility regions. The guard should flag the risky text even if the page renders it poorly.

Indirect prompt injection through fetched content

A second test is fetched HTML, markdown, or docs that contain instructions aimed at the model. The important failure mode is not whether the text is visible. It is whether your app treats the text as instructions.

False positives and safe fallback behavior

False positives will happen. The safe response is not to force the model through anyway. Fall back to:

  • read-only mode
  • user confirmation
  • truncated context
  • manual review

That is better than silently trusting suspicious content.

Backend checks still matter

Why client-side defenses are only one layer

Client-side guards help, but they run in an environment the attacker can often influence. They are useful for reducing accidental misuse and catching obvious cases, not for enforcing security on their own.

Server-side authorization and tool validation

The backend must still verify:

  • the user owns the resource
  • the action is permitted
  • the tool call matches the current session
  • the request is valid even if the client lies

If a client-side guard blocks a malicious instruction but the server accepts the same action from a forged request, the real bug is still on the backend.

Practical limits and next steps

Tuning thresholds for your application

A summary app and an autonomous agent do not need the same threshold. If your product can tolerate extra confirmations, be stricter. If it must process lots of messy content, use more nuanced scoring and stronger allowlists.

Expanding detection with allowlists and schemas

After the first pass works, move toward structure:

  • allow only known-safe tool names
  • require schemas for tool arguments
  • mark source fields by trust level
  • strip instruction-like text from low-trust content

That usually beats piling on more regexes.

Conclusion

A client-side prompt injection guard is worth building because it gives you a concrete place to separate user intent from untrusted content. It will not replace backend authorization, and it will not catch every attack. It does make the browser app less eager to treat hostile text as a command, which is exactly where a lot of AI bugs start.

Share this post

More posts

Comments