Skip to content
← Back to Blog

Security Work in the Frontend: PII Masking, Audit Logging, and Permission Surfaces

· 6 min read
securityfrontendprivacyplatformarchitecture

When people hear “frontend security,” they often think it’s just:

  • avoid storing long-lived auth tokens in localStorage (XSS can read it)
  • sanitize user input
  • hide admin-only buttons

Those things matter, but in most real products the frontend is also the place where:

  • PII is displayed (and copied, screenshotted, exported)
  • privileged actions are initiated
  • audit trails begin (or silently don’t)
  • permissions become a user experience surface

This post is the practical security work I’ve found myself doing in the frontend: PII masking, audit logging, and ACL surfaces. Not as isolated features - as one coherent threat model.

Start with a threat model (even a lightweight one)

You don’t need a 20-page document. You need clarity on:

  • Assets: what data/actions are sensitive? (PII, financial data, admin actions)
  • Actors: who might misuse it? (legit users, insiders, compromised accounts)
  • Channels: where can it leak? (UI, logs, analytics, screenshots, exports)
  • Controls: what prevents/limits damage? (masking, permissions, audit, rate limits)

The key insight: frontend leaks are often “incidental”. Not hackers. Just normal behavior:

  • copy/paste into tickets
  • screenshots in Slack
  • debug logs left in prod
  • analytics capturing “helpful” metadata

PII masking as a product feature (not a checkbox)

Masking isn’t “hide the value.” It’s: show the right amount of information to the right person at the right time.

Classify fields

We started with a simple field taxonomy:

  • Public: safe to show and log
  • Sensitive: can show with permission; never log raw
  • Highly sensitive: show only with elevated permission + explicit reveal + audit event

Examples:

  • email: sensitive
  • phone: sensitive
  • full address: highly sensitive (depends on product)
  • government ID: highly sensitive

Default to masked display

Mask by default in UI:

  • j***@company.com
  • +84 *** *** 123

Then offer a reveal affordance:

  • click “Reveal”
  • require elevated permission
  • optionally require step-up authentication for very sensitive fields

“Reveal” must be auditable

If a user can reveal PII, that action should be an audit event:

  • who revealed
  • what field type (not necessarily the raw value)
  • which entity (customer ID)
  • when
  • from where (screen/action)

Protect copy flows

Copy is where PII escapes.

We implemented:

  • “Copy masked” and “Copy full” as separate actions
  • “Copy full” gated by permission
  • “Copy full” emits an audit event
  • redaction in UI to prevent accidental selection (e.g., reveal-on-hold vs always visible)

Don’t leak via search and autocomplete

Search results often show “helpful snippets.” That’s a leakage channel.

Decide explicitly:

  • what fields are searchable
  • what fields can appear in results
  • what is shown on hover vs click

Treat search as a security surface, not just a UX feature.

“Don’t leak in logs” is harder than it sounds

PII leaks into logs through:

  • debug statements (console.log(customer))
  • error tracking payloads
  • analytics events
  • network inspectors / middleware logs

Redact at the edges

Defense-in-depth matters:

  • client-side redaction before sending logs/analytics
  • server-side redaction as a backstop

Client-side is especially important because:

  • analytics often bypass your backend
  • error monitoring SDKs capture breadcrumbs automatically

Use allowlists, not denylists

If you try to list “all PII fields,” you will miss something.

A safer approach:

  • define a log schema
  • allowlist the fields that are safe
  • drop everything else by default

Treat error payloads as hostile

Errors often contain raw request/response bodies.

We made it a rule:

  • never attach raw response bodies to error reports in production
  • extract safe metadata (status code, endpoint, correlation ID)

If you need deeper context, gate it behind:

  • sampling
  • on-call-only toggles
  • ephemeral access

Permissions as a UX surface (ACL surfaces)

There are three common anti-patterns:

  1. Hide everything: users don’t know what exists.
  2. Show everything: users click and get “403” everywhere.
  3. Disable everything silently: users don’t know why.

The best pattern I’ve found:

  • show the action
  • disable it if not permitted
  • explain why (and what to do)

Example:

  • “Refund” button disabled
  • tooltip: “Requires billing.refund permission. Request access from your admin.”

This reduces support tickets and makes the system feel trustworthy.

Important: UI is not enforcement

The backend must enforce permissions.

But frontend still matters because:

  • it reduces error spam
  • it prevents accidental attempts
  • it communicates policy clearly

Think of frontend permission checks as policy UX, not security.

Audit logging: make events trustworthy

Audit logs are only useful if:

  • events are complete
  • events are hard to tamper with
  • events can be correlated across systems

Capture intent and outcome

For privileged actions, log both:

  • intent (user clicked “Refund”)
  • outcome (refund succeeded/failed, with reason category)

This matters when investigating:

  • abuse attempts
  • permission misconfigurations
  • operational mistakes

Use correlation IDs everywhere

If you have tracing/correlation IDs, include them in:

  • frontend events
  • backend logs
  • audit events

So “what happened?” becomes one query, not archaeology.

Tamper-evident audit events

You can’t truly make audit logs tamper-proof in the frontend.

But you can design the system so tampering is detectable:

  • generate audit events on the server whenever possible
  • when frontend emits events, sign and store on the server
  • store audit logs append-only
  • consider hash chaining (each event includes hash of previous event) for sensitive domains

The frontend’s job is to:

  • emit the right intent metadata
  • avoid leaking raw PII in the event payload

The edge cases that surprised us

  • Exports: CSV/PDF exports are PII exfiltration features unless designed carefully.
  • Bulk actions: “select all” + “apply” needs stricter audit detail (how many? which filter?).
  • Screenshots: if users share screenshots, masking-by-default helps. Don’t assume they won’t.
  • Third-party UI libs: toasts/modals sometimes stringify objects (hello, accidental PII).
  • Caching: don’t cache sensitive responses in places that survive logouts.

A checklist you can steal

  • Field classification (public/sensitive/highly sensitive)
  • Mask-by-default UI components
  • Permission-gated reveal + copy flows
  • Search results explicitly designed for sensitive data
  • Client-side redaction for logs/analytics/error monitoring
  • Allowlisted logging schemas
  • Consistent permission UX (disabled + explainable)
  • Server-side enforcement and server-generated audit events when possible
  • Correlation IDs across frontend/backend/audit

Frontend security work is mostly about preventing “normal behavior” from becoming a data leak. When you treat masking, permissions UX, and audit logging as one system, the product becomes both safer and easier to operate.