Security Work in the Frontend: PII Masking, Audit Logging, and Permission Surfaces
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:
- Hide everything: users don’t know what exists.
- Show everything: users click and get “403” everywhere.
- 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.refundpermission. 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.