React Forum
Blog & Guide Responses
Jun 21, 2022 · 04:00 PM

[Deep Dive #11] Security Guardrails - discussion

Started by Soren Malik
blogsecurityguardrailsroutingevidenceposture

Security guardrails in UI code often start as a pile of redirects and special cases until you have an incident and realize nothing is reproducible.

Read the post

Do you model auth/permission posture as route truth (so redirects and access denials are reproducible), or keep it local to components? What evidence do you render so support can tell why a user got blocked or redirected without digging into logs? How do you avoid guardrails turning into distributed checks sprinkled across the tree?

Comments (10)

Back to latest
Maya Henson
Jun 21, 2022 · 04:06 PM

I like the idea of rendering why you blocked, but doesn't that risk leaking sensitive info in the UI?

How do you keep evidence safe?

Soren Malik
Jun 21, 2022 · 04:10 PM

We keep evidence coarse: "blocked by auth" vs "blocked by role".

The evidence's job is to make behavior explainable, not to expose policy internals.

Maya Henson
Jun 21, 2022 · 04:13 PM

That makes sense. I was imagining evidence as basically a policy dump.

Dmitri Kline
Jun 21, 2022 · 04:18 PM

We treat auth posture as route truth because it affects navigation.

If you don't, you end up with effects that redirect after render and it's both janky and hard to reproduce.

This is basically the "make it observable" stance from Security and Safety and Routing and Navigation.

Salma Qureshi
Jun 21, 2022 · 04:26 PM

The only way we avoided distributed checks was centralizing guardrails into routines owned by the shell.

Screens don't decide. Screens ask.

Hana Saito
Jun 21, 2022 · 04:34 PM

Concrete guard routine pattern (keeps it testable and evidence-friendly):

ts
type GuardResult = { ok: true } | { ok: false; kind: 'auth' | 'role'; action: 'redirect' | 'deny' };

function requireRole(shell: any, role: string): GuardResult {
  const user = shell.read('user');
  const ok = Boolean(user) && user.roles?.includes(role);
  if (ok) return { ok: true };
  const res: GuardResult = { ok: false, kind: user ? 'role' : 'auth', action: user ? 'deny' : 'redirect' };
  shell.write('guardEvidence', { kind: res.kind, action: res.action });
  return res;
}

Now tests assert on guardEvidence and the UI can render a safe, coarse explanation.

Keira Santos
Jun 21, 2022 · 04:45 PM

Counterpoint: for simple apps, putting auth checks into every fetch layer might be enough.

Do we really need route truth for auth posture if all you're doing is showing a login screen?

Theo Bennett
Jun 21, 2022 · 04:50 PM

If auth changes navigation, you benefit from making it route-visible. Even simple apps have "why did it redirect" bugs.

Also: fetch-layer-only tends to turn into a bunch of invisible retries and redirects. Evidence keeps it honest.

Amina Farouk
Jun 21, 2022 · 04:58 PM

We hit a prod incident where anonymous users saw a cached logged-in page from an old tab.

Once we added an auth lane evidence key (and made cache posture depend on it), the class of bug became obvious and testable.

Jules Kwon
Jun 21, 2022 · 05:10 PM

The best part of guardrails-as-contract is you can snapshot it. A screenshot can show guardEvidence=auth:redirect and support knows what happened.

Inez Park
Jun 21, 2022 · 05:22 PM

Also: keep evidence stable and boring. If you change evidence shape every sprint, you lose the ability to use it in tests and triage.

Treat it like API.

We borrowed a lot of the "render-visible contract signal" language from Testing and Debugging.

Benji Rios
Jun 21, 2022 · 05:36 PM

We keep two evidence keys: auth=anon|user and guard=ok|redirect|deny. That's enough most of the time.

Grace McCoy
Jun 21, 2022 · 05:49 PM

If a guardrail exists, it needs a place to live. Otherwise you end up with twelve "just in case" checks and no one knows which one mattered.