# Client Side Trust Boundary

> Client-Side Trust Boundary

- Skill: `annatchijova/client-side-trust-boundary` (Agent Skill)
- Install (CLI): `npx skillmds@latest add annatchijova/client-side-trust-boundary`
- Raw SKILL.md: https://api.skillmd.com/api/skills/annatchijova/client-side-trust-boundary/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: annatchijova (https://skillmd.com/u/annatchijova)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/annatchijova/client-side-trust-boundary

---


# Client-Side Trust Boundary

The line where your code stops running on hardware you control and starts running on
the user's device is the most under-respected trust boundary in software. Past that
line, everything is negotiable: the attacker owns the CPU, the memory, the storage, the
network stack, and the debugger. They can root the phone, attach Frida and rewrite any
function at runtime, bypass the certificate pin, dump the "encrypted" SQLite with the
key sitting three files over, decompile the APK, and flip the `isPremium` boolean. **A
security decision that is made only on the client has not been made.** It has been
suggested to an adversary who is free to decline.

Two failure modes, and both come from mistaking "the code we shipped" for "the code
that runs":

- **Trusting a client-side check.** The disabled button, the client-validated price,
  the feature gate, the input validation, the "jailbreak detected → refuse" guard — the
  attacker simply removes it. Worse, root/jailbreak detection and anti-tamper are
  themselves client-side code, so they are defeated by the same instrumentation they
  were meant to catch. A control that runs in the attacker's process protects nothing.
- **Hiding a secret on the client.** The hardcoded API key, the "encrypted" local store
  whose key is derived from something also on the device, the obfuscated algorithm. A
  secret that ships to the client is a disclosed secret; obfuscation raises the cost of
  reading it from minutes to an afternoon, which is not a boundary.

Composes with the library:

- **validate-at-the-boundary** — the server-side corollary: every client input and every client decision is re-validated on trusted ground or it is unvalidated
- **agent-trust-boundaries** — the client is an untrusted execution context whose output cannot carry authority
- **authorization-surface-mapping** — the client can craft any request; the server's authorization is the only real gate (IDOR/BOLA is this failure)
- **secret-lifecycle-discipline** — a secret embedded in a client is already leaked; treat it as compromised from ship
- **credential-material-triage** — an extracted client key is captured material; triage what it actually unlocks
- **honest-degradation** — a control that can only live client-side (offline, DRM) is defeatable; design for the loss, do not pretend it holds

---

## Step 0 — Draw the boundary and declare the far side hostile

Locate, explicitly, where execution crosses onto the user's device. Everything past
that point — app logic, local storage, in-memory values, outbound requests, TLS
endpoints the app talks to — is attacker-influenced. This is Firstness done honestly:
not "the app does X," but "the app *asks* the device to do X, and the device answers to
the user." Write the boundary before you reason about any control, because every claim
downstream depends on which side of it the control lives.

---

## Step 1 — Enumerate the client-side decisions and treat each as void

List every security-relevant decision the client makes, and mark each one **not a
control until it is re-made server-side**:

- Authentication/authorization checks ("is this user allowed to see this screen")
- Input validation ("the app won't let you submit a negative quantity")
- Pricing/business logic ("total computed in the app")
- Feature/entitlement gates ("premium unlocked locally")
- Integrity/anti-tamper/root-jailbreak detection

For each, the question is: *if the attacker deletes or flips this, what stops the
abuse?* If the answer is "nothing, the server trusts the client's word," that is the
finding. The server must independently recompute the price, re-check the entitlement,
re-authorize the action — because the request it receives may have been crafted by hand
with no app involved at all.

---

## Step 2 — Treat every shipped secret as disclosed

Anything the client must *have* to function, the attacker can *extract*:

- Hardcoded API keys, tokens, signing keys → disclosed; triage their blast radius
  (`credential-material-triage`) and prefer a design where the client holds nothing of
  value (server-side proxying, per-user short-lived tokens minted after real auth).
- "Encrypted" local storage whose key is on the same device → the key travels with the
  ciphertext; this is encoding, not protection.
- Obfuscation → a cost multiplier, not a boundary. State honestly that it deters casual
  and scaled attackers and does nothing against a targeted one (base rate: a determined
  reverser with Frida/decompilers reads it routinely).

---

## Step 3 — Separate what the client CAN do from what the server LETS it do

The client can issue *any* request — any endpoint, any object id, any parameter,
regardless of what the UI exposes. Capability is unbounded; the only real gate is
server-side authority. Every "the app only ever requests its own data" assumption is
an IDOR/BOLA waiting to happen. Route authorization through
`authorization-surface-mapping`: the server must check, on every request, that *this*
principal may perform *this* action on *this* object — never inferring permission from
the fact that a well-behaved client would not have asked.

---

## Step 4 — Rate anti-tamper honestly: speed bumps, not walls

Certificate pinning, root/jailbreak detection, integrity attestation, obfuscation, and
anti-debugging are worth having — they raise attacker cost, cut down casual and
automated abuse, and are appropriate defense-in-depth. But they are speed bumps, and
naming them as boundaries is where teams get breached. State for each what it actually
buys (deters scaled/casual attacks, adds reverse-engineering cost) and what it does not
(stops a targeted attacker instrumenting the process). Never let a security property
*rest* on one. Play integrity / hardware attestation is the partial exception — it
anchors below the app — and belongs to `root-of-trust-reasoning`, not to app code.

---

## Step 5 — When a control can only live client-side, design for its defeat

Some requirements are inherently client-side: offline-first enforcement, DRM,
rate-limiting a fully local action. You cannot make these unbreakable. Honest
degradation means: assume the control is defeated, and bound the damage —
server-side reconciliation that detects the tampered state after the fact, limits on
what a compromised client can affect, and monitoring that treats client-reported state
as a claim, not a fact. Say in the design "this is enforced only on the client and is
therefore defeatable; here is what limits the blast radius," instead of shipping a
client check and calling the requirement met.

---

## The one-line test

If the only thing standing between an attacker and the abuse runs on the attacker's own
device, then nothing stands between them and the abuse. Move the check to the server, or
write down plainly that the requirement is unenforced and design for the loss.

