Deepen PracticeOrdered learning track

Sensitive Data Exposure in UI

Learn React Authentication, Authorization, Identity & Permission/ACL - Part 086

Sensitive data exposure di React authenticated UI: DOM, props, logs, analytics, error boundary, source maps, screenshots, cache, URL, browser extension, dan observability.

10 min read1828 words
PrevNext
Lesson 86130 lesson track72–107 Deepen Practice
#react#authentication#authorization#identity+4 more

Part 086 — Sensitive Data Exposure in UI

Frontend security sering dibahas sebagai XSS, CSRF, token storage, dan headers. Tapi untuk aplikasi authenticated, salah satu failure paling sering adalah hal yang lebih tenang:

Data sensitif sudah sampai ke browser padahal UI tidak benar-benar membutuhkan data itu.

Ketika data masuk browser, data dapat muncul di:

  • DOM
  • React props
  • serialized SSR payload
  • query cache
  • local/session storage
  • service worker cache
  • browser history
  • URL
  • logs
  • analytics
  • error reports
  • screenshots
  • copy/paste
  • browser extension
  • devtools
  • source maps
  • CDN/cache/debug endpoints

Authorization mungkin benar di API, tetapi exposure tetap gagal karena server mengirim terlalu banyak data ke client.

Mental model:

The safest sensitive field is the field never sent to the browser.

1. Sensitive data exposure bukan hanya “data leak”

Dalam React app, sensitive exposure punya banyak bentuk.

ExposureContohDampak
Over-fetchingAPI mengirim full user profilePII tersedia di devtools/cache
Hidden DOMField disembunyikan CSSData tetap bisa dibaca
Serialized SSRSecret ada di hydration payloadView-source/network leak
Error boundaryStack/error response berisi tokenSentry/log leak
AnalyticsURL/props dikirim ke vendorPII keluar organisasi
Query cacheTanStack Query menyimpan record sensitifData masih ada setelah logout
Source mapInternal route/env/logic bocorReconnaissance
Console logDebug data productionExtension/devtools/log collector leak
ScreenshotUI berisi PII di shared support toolUnauthorized disclosure
ClipboardCopy action mengambil hidden dataAccidental leak

Top-tier React engineer tidak hanya bertanya “apakah button disembunyikan?”, tetapi “apakah data itu pernah dikirim ke browser?”.


2. Data sensitivity classification

Sebelum membuat UI, klasifikasikan data.

type Sensitivity =
  | 'public'
  | 'internal'
  | 'confidential'
  | 'restricted'
  | 'secret';

Contoh mapping:

SensitivityContohFrontend treatment
publicproduct name, published docsboleh cache publik
internalinternal case titleauth required, no public cache
confidentialPII, email, phone, addressminimum projection, no analytics
restrictedlegal notes, investigation detailsrole/action/resource auth, mask by default
secrettoken, password, private key, recovery codenever send to React except one-time ceremony if unavoidable

Auth UI harus punya type-level awareness:

type SensitiveField<T> = {
  value: T;
  sensitivity: Sensitivity;
  display: 'plain' | 'masked' | 'redacted' | 'one_time';
};

Tapi jangan over-engineer hanya di client. Klasifikasi harus muncul dalam API projection contract.


3. Projection: jangan kirim domain entity mentah ke UI

Bad API response:

{
  "id": "CASE-123",
  "title": "Potential fraud report",
  "customerName": "Jane Doe",
  "customerEmail": "jane@example.com",
  "nationalId": "1234567890",
  "internalRiskScore": 92,
  "investigatorNotes": "...",
  "allowedActions": ["case.view"]
}

UI mungkin hanya butuh:

{
  "id": "CASE-123",
  "title": "Potential fraud report",
  "customer": {
    "displayName": "Jane D.",
    "emailMasked": "j***@example.com"
  },
  "allowedActions": ["case.view"]
}

Gunakan API projection:

type CaseListItemDto = {
  id: string;
  title: string;
  customerDisplayName: string;
  status: 'open' | 'closed' | 'escalated';
  allowedActions: Array<'case.view' | 'case.assign'>;
};

type CaseSensitiveDetailDto = {
  id: string;
  title: string;
  customerEmail: string;
  nationalIdMasked: string;
  riskExplanation: string;
  allowedActions: Array<'case.view_sensitive' | 'case.export'>;
};

List endpoint jangan mengirim sensitive detail. Detail endpoint juga harus field-level authorized.


4. Hidden UI is not hidden data

Anti-pattern:

function CustomerPanel({ customer, canViewSensitive }: Props) {
  return (
    <section>
      <div>{customer.name}</div>
      <div hidden={!canViewSensitive}>{customer.nationalId}</div>
    </section>
  );
}

Masalah:

  • nationalId sudah ada di React props
  • data muncul di memory
  • bisa terlihat di React DevTools
  • bisa bocor ke error log
  • bisa dibaca XSS
  • bisa bocor via copy/screenshot extension

Better:

function CustomerPanel({ customer }: { customer: CustomerProjection }) {
  return (
    <section>
      <div>{customer.name}</div>
      {customer.sensitive?.nationalIdMasked ? (
        <div>{customer.sensitive.nationalIdMasked}</div>
      ) : (
        <div>Restricted</div>
      )}
    </section>
  );
}

Dan paling penting: customer.sensitive hanya dikirim jika server memutuskan user boleh melihat projection tersebut.


5. DOM exposure

DOM adalah data surface.

Risiko:

<div data-user-email={user.email}>{user.name}</div>

data-* attribute sering dipakai untuk testing/analytics. Jangan isi dengan PII.

Bad:

<button data-account-id={account.id} data-email={account.email}>
  Suspend
</button>

Better:

<button data-testid="suspend-account-button">
  Suspend
</button>

Testing identifier bukan data channel.


6. URL exposure

URL adalah salah satu tempat paling mudah bocor.

Jangan taruh ini di URL:

/reset?token=secret
/export?jwt=...
/case?customerEmail=jane@example.com
/callback?code=...  // unavoidable in OAuth, but must be short-lived and cleaned immediately

URL bisa masuk ke:

  • browser history
  • server access logs
  • reverse proxy logs
  • analytics
  • referrer header
  • screenshots
  • support tools
  • chat previews
  • copied links

OAuth callback code memang lewat URL, tetapi harus segera diproses dan URL dibersihkan.

function cleanupCallbackUrl() {
  window.history.replaceState(null, '', '/auth/callback/complete');
}

Untuk sensitive filters, gunakan server-side saved search ID atau POST body jika sesuai.


7. Referrer leakage

Jika authenticated page memuat third-party resource, browser dapat mengirim referrer.

Contoh:

https://app.example.com/cases/CASE-123?customer=jane@example.com

Request ke image/CDN/analytics pihak ketiga dapat membawa referer tergantung policy.

Minimum:

Referrer-Policy: strict-origin-when-cross-origin

Untuk halaman sangat sensitif:

Referrer-Policy: no-referrer

Juga jangan desain route yang memuat PII di path/query.

Bad:

/customers/jane@example.com

Better:

/customers/cus_123

Lebih baik lagi: opaque ID yang tetap divalidasi object-level authorization.


8. React props dan component boundary

Client Component boundary adalah exposure boundary.

Bad:

<UserCard user={fullUserEntity} />

Better:

type UserCardProps = {
  displayName: string;
  avatarUrl?: string;
  roleLabel?: string;
};

<UserCard
  displayName={user.displayName}
  avatarUrl={user.avatarUrl}
  roleLabel={user.roleLabel}
/>

Untuk RSC/SSR, jangan pass entity penuh ke Client Component. Serialize only what the client needs.

// Server Component
export async function UserCardBoundary({ userId }: { userId: string }) {
  const projection = await getUserCardProjection(userId);
  return <UserCard {...projection} />;
}

9. Serialized SSR/RSC payload

SSR membuat exposure baru: data bisa muncul di HTML atau hydration payload.

Risiko:

<script id="__DATA__" type="application/json">
  {JSON.stringify(fullSession)}
</script>

Masalah:

  • session claims bocor
  • token accidentally serialized
  • internal role mapping terlihat
  • policy trace terlihat
  • PII ada di page source

Rule:

Never serialize token, secret, raw policy trace, full user profile, or unrestricted domain entity into HTML.

Session projection yang aman:

type PublicSessionProjection = {
  user: {
    id: string;
    displayName: string;
  };
  tenant: {
    id: string;
    slug: string;
  };
  authEpoch: number;
  permissionEpoch: number;
};

Bukan:

type UnsafeSessionProjection = {
  accessToken: string;
  refreshToken: string;
  email: string;
  groups: string[];
  rawIdToken: string;
  policyDebug: unknown;
};

10. Query cache exposure

TanStack Query/Apollo/Relay cache adalah memory store. Ia bukan secure vault.

Risiko:

  • sensitive data tetap ada setelah logout
  • tenant A data muncul setelah switch ke tenant B
  • persisted cache menyimpan PII di IndexedDB/localStorage
  • devtools memperlihatkan data
  • stale permission projection tetap membuat UI terlihat allowed

Pattern:

function onLogout(queryClient: QueryClient) {
  queryClient.cancelQueries();
  queryClient.clear();
}

Tenant-aware key:

const caseKey = (tenantId: string, caseId: string, permissionEpoch: number) => [
  'tenant', tenantId,
  'case', caseId,
  'permissionEpoch', permissionEpoch,
] as const;

Persisted cache policy:

type PersistPolicy = 'never' | 'memory_only' | 'encrypted_at_rest' | 'redacted_projection_only';

Untuk sensitive authenticated UI, default terbaik adalah no persisted cache kecuali ada kebutuhan eksplisit dan mitigasi matang.


11. Local/session storage exposure

Jangan simpan data sensitif di localStorage/sessionStorage.

Bad:

localStorage.setItem('user', JSON.stringify(fullUserProfile));
localStorage.setItem('permissions', JSON.stringify(allPolicies));
localStorage.setItem('lastCase', JSON.stringify(caseDetail));

Better:

sessionStorage.setItem('lastRouteGroup', 'case-detail');

Atau tidak simpan sama sekali.

Jika harus menyimpan preference:

type SafeUiPreference = {
  theme: 'light' | 'dark' | 'system';
  compactTable: boolean;
};

Preference bukan identity data.


12. Service worker cache exposure

Service worker dapat menyimpan response authenticated jika tidak dikontrol.

Bad:

self.addEventListener('fetch', (event) => {
  event.respondWith(caches.open('app').then(async (cache) => {
    const cached = await cache.match(event.request);
    if (cached) return cached;
    const response = await fetch(event.request);
    cache.put(event.request, response.clone());
    return response;
  }));
});

Ini bisa menyimpan /api/me, /api/cases/123, /api/export, dan data sensitif.

Better:

function isCacheableStaticAsset(request: Request): boolean {
  const url = new URL(request.url);
  return request.method === 'GET'
    && url.origin === self.location.origin
    && url.pathname.startsWith('/assets/');
}

Authenticated API response default:

Cache-Control: no-store

Logout harus membersihkan cache jika service worker aktif.

async function clearAppCaches() {
  const names = await caches.keys();
  await Promise.all(names.map((name) => caches.delete(name)));
}

13. Console logs

Debug log di React sering menjadi leak.

Bad:

console.log('session', session);
console.log('token', accessToken);
console.log('case', caseDetail);
console.error('api error', error.response);

Production logger harus redaction-first.

type LogValue = string | number | boolean | null | undefined;

type SafeLogEvent = {
  event: string;
  requestId?: string;
  userIdHash?: string;
  tenantId?: string;
  routeGroup?: string;
  status?: number;
};

function logSecurityEvent(event: SafeLogEvent) {
  // Forward to controlled logger; no arbitrary objects.
}

Rule:

Do not log arbitrary objects from auth, API response, form state, or error response.

14. Error boundary leakage

React error boundary sering menampilkan atau mengirim stack/context.

Bad:

function ErrorFallback({ error }: { error: Error }) {
  return <pre>{JSON.stringify(error, null, 2)}</pre>;
}

Bad error reporter:

captureException(error, {
  extra: {
    session,
    currentCase,
    formValues,
    apiResponse,
  },
});

Better:

type ErrorReportContext = {
  requestId?: string;
  routeGroup?: string;
  status?: number;
  authState: 'anonymous' | 'authenticated' | 'expired' | 'forbidden';
};

captureException(error, {
  tags: {
    routeGroup: context.routeGroup,
    authState: context.authState,
  },
  extra: {
    requestId: context.requestId,
    status: context.status,
  },
});

User-facing fallback:

function AuthSafeErrorFallback({ requestId }: { requestId?: string }) {
  return (
    <section>
      <h1>Something went wrong</h1>
      <p>Try again. If the problem continues, contact support.</p>
      {requestId ? <p>Reference: {requestId}</p> : null}
    </section>
  );
}

15. Analytics exposure

Analytics SDK sering merekam:

  • full URL
  • page title
  • user ID/email
  • button text
  • form field value
  • custom event properties
  • session replay
  • DOM snapshots

Bad:

analytics.identify(user.id, {
  email: user.email,
  name: user.name,
  role: user.role,
  tenantName: tenant.name,
});

analytics.track('case_opened', {
  caseId: case.id,
  customerEmail: case.customer.email,
  riskScore: case.riskScore,
});

Better:

analytics.identify(hashUserId(user.id), {
  plan: tenant.plan,
});

analytics.track('case_opened', {
  routeGroup: 'case-detail',
  status: case.status,
  hasSensitiveView: false,
});

Untuk session replay:

  • disable pada sensitive route
  • mask all inputs by default
  • block DOM regions containing PII
  • do not record password/MFA/passkey ceremonies
  • review vendor data retention and access control

16. Form fields dan autocomplete

Sensitive forms perlu policy eksplisit.

<input
  name="nationalId"
  autoComplete="off"
  inputMode="numeric"
/>

Namun autocomplete="off" bukan strong security control. Browser/password manager behavior bisa bervariasi.

Untuk secrets:

<input
  name="recoveryCode"
  type="text"
  autoComplete="one-time-code"
/>

Untuk password:

<input
  name="password"
  type="password"
  autoComplete="current-password"
/>

Jangan menyimpan form draft sensitif otomatis di localStorage.

Bad:

localStorage.setItem('draft_case_notes', notes);

Better:

// Server-side draft with authorization, audit, TTL, and explicit save intent.
await saveDraft({ caseId, notes });

17. Clipboard and export

Copy/export adalah data egress.

Bad:

<button onClick={() => navigator.clipboard.writeText(JSON.stringify(caseDetail))}>
  Copy
</button>

Better:

type CopyProjection = {
  caseId: string;
  title: string;
  status: string;
};

function buildCopyText(projection: CopyProjection) {
  return `Case ${projection.caseId}: ${projection.title} (${projection.status})`;
}

Export harus authorization action sendiri:

case.export_sensitive_data

Bukan otomatis sama dengan case.view.


18. Screenshot risk

React app tidak bisa sepenuhnya mencegah screenshot. Maka desain harus mengurangi exposure.

Patterns:

  • mask PII by default
  • reveal sensitive field by explicit click
  • time-box reveal
  • show watermark for regulated/admin/impersonation views
  • avoid loading sensitive data in list/table view
  • disable session replay on sensitive routes
  • use print/export permission separately

Contoh reveal pattern:

function SensitiveValue({ masked, loadPlain }: Props) {
  const [plain, setPlain] = React.useState<string | null>(null);

  async function reveal() {
    const value = await loadPlain(); // server re-checks permission + step-up if required
    setPlain(value);
    window.setTimeout(() => setPlain(null), 30_000);
  }

  return plain ? (
    <span>{plain}</span>
  ) : (
    <button onClick={reveal}>Reveal {masked}</button>
  );
}

Reveal action harus diaudit.


19. Source maps

Source maps membantu debugging, tetapi di production bisa memberi attacker:

  • route structure
  • internal function names
  • feature flags
  • policy names
  • API endpoint names
  • comments/TODO
  • internal error messages

Source maps bukan biasanya tempat secret jika build benar, tetapi tetap reconnaissance surface.

Policy:

  • jangan embed secret di client bundle
  • upload source maps ke private error tracking jika perlu
  • jangan serve source maps public untuk high-risk apps
  • review build artifacts
  • scan bundle untuk accidental secrets

Build-time check:

grep -R "BEGIN PRIVATE KEY\|refresh_token\|client_secret\|AWS_SECRET" dist/ || true

Lebih baik gunakan automated secret scanning.


20. Environment variables

React/browser bundle tidak punya server secret.

Bad:

const clientSecret = import.meta.env.VITE_CLIENT_SECRET;

Semua env dengan prefix public akan masuk bundle.

Next.js juga membedakan server env dan browser-exposed env melalui prefix seperti NEXT_PUBLIC_.

Rule:

Anything shipped to browser is public.

Client boleh punya:

public OAuth client ID
public app environment name
public analytics write key with constrained scope

Client tidak boleh punya:

OAuth client secret
API signing secret
private key
database URL
service account credential
refresh token vault secret

21. Browser extensions and devtools

Browser extensions bisa membaca DOM, network, storage, dan page content sesuai permission mereka. React app tidak bisa menganggap browser sebagai trusted vault.

Implikasi:

  • HttpOnly cookie mengurangi token theft dari JS, tetapi page content tetap bisa dibaca jika sudah dirender
  • hidden DOM tetap bisa dibaca
  • localStorage/sessionStorage lebih mudah dieksfiltrasi
  • sensitive fields harus minimize-by-default
  • high-risk action perlu step-up dan server-side logging

Jangan mengirim secret ke browser dengan alasan “nanti tidak ditampilkan”.


22. Source of truth: policy projection vs raw policy trace

Policy engine mungkin menghasilkan trace:

{
  "decision": "deny",
  "rule": "case_sensitive_view_requires_investigator_or_supervisor",
  "matchedAttributes": {
    "subject.groups": ["support_l1"],
    "resource.riskScore": 92,
    "resource.flags": ["aml_review"]
  }
}

Jangan kirim ini ke client.

Client cukup menerima:

{
  "decision": "deny",
  "reason": "requires_higher_permission",
  "requestAccess": {
    "allowed": true,
    "target": "case.view_sensitive"
  }
}

Policy trace untuk internal audit/debugging, bukan UI.


23. Sensitive error response

Bad API error:

{
  "error": "Forbidden",
  "debug": "User user_123 lacks case.view_sensitive for AML flagged case owned by team Fraud-East",
  "policyTrace": {...},
  "sql": "select * from cases ..."
}

Better:

{
  "type": "https://docs.example.com/problems/forbidden",
  "title": "Access denied",
  "status": 403,
  "code": "permission_denied",
  "requestId": "req_abc123"
}

Internal logs can correlate by requestId.


24. Data minimization in API design

Implement projection at API boundary.

type ProjectionContext = {
  subjectId: string;
  tenantId: string;
  permissions: Set<string>;
  purpose: 'list' | 'detail' | 'export' | 'audit';
};

function projectCase(entity: CaseEntity, ctx: ProjectionContext) {
  const base = {
    id: entity.id,
    title: entity.title,
    status: entity.status,
  };

  if (!ctx.permissions.has('case.view_sensitive')) {
    return {
      ...base,
      customer: {
        displayName: maskName(entity.customer.name),
      },
    };
  }

  return {
    ...base,
    customer: {
      displayName: entity.customer.name,
      email: entity.customer.email,
      nationalIdMasked: maskNationalId(entity.customer.nationalId),
    },
  };
}

Server projection beats client hiding.


25. Route-level sensitivity profile

Setiap route dapat punya sensitivity profile.

type RouteSensitivity = {
  routeGroup: string;
  sensitivity: Sensitivity;
  allowAnalytics: boolean;
  allowSessionReplay: boolean;
  cachePolicy: 'no-store' | 'private-short' | 'static';
  referrerPolicy: 'no-referrer' | 'strict-origin-when-cross-origin';
  requireWatermark: boolean;
};

Contoh:

const routeSensitivity: Record<string, RouteSensitivity> = {
  caseDetail: {
    routeGroup: 'case-detail',
    sensitivity: 'restricted',
    allowAnalytics: true,
    allowSessionReplay: false,
    cachePolicy: 'no-store',
    referrerPolicy: 'no-referrer',
    requireWatermark: true,
  },
};

Route sensitivity mempengaruhi:

  • headers
  • analytics masking
  • error reporting context
  • session replay policy
  • print/export capability
  • layout watermark
  • data projection

26. Sensitive UI checklist per component

Untuk setiap component yang menerima data sensitif, tanyakan:

  • Apakah field ini benar-benar dibutuhkan di browser?
  • Apakah server sudah memproyeksikan field minimal?
  • Apakah data muncul di DOM attribute?
  • Apakah data masuk query cache?
  • Apakah data bisa masuk logs/error reporting?
  • Apakah data masuk analytics/session replay?
  • Apakah data disimpan di localStorage/sessionStorage/IndexedDB?
  • Apakah data masih ada setelah logout/tenant switch?
  • Apakah field perlu masked by default?
  • Apakah reveal action perlu step-up/auth audit?
  • Apakah export/copy/print punya permission sendiri?

27. Redaction utilities

Centralize redaction.

type Redactable = string | number | boolean | null | undefined | Record<string, unknown> | unknown[];

const SENSITIVE_KEYS = [
  'token',
  'accessToken',
  'refreshToken',
  'idToken',
  'password',
  'secret',
  'authorization',
  'cookie',
  'nationalId',
  'email',
  'phone',
];

function redact(value: Redactable): Redactable {
  if (Array.isArray(value)) return value.map(redact);

  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value).map(([key, nested]) => {
        const sensitive = SENSITIVE_KEYS.some((pattern) =>
          key.toLowerCase().includes(pattern.toLowerCase()),
        );

        return [key, sensitive ? '<redacted>' : redact(nested as Redactable)];
      }),
    );
  }

  return value;
}

Namun jangan jadikan redaction sebagai izin untuk log everything. Redaction adalah safety net, bukan architecture.


28. Testing sensitive data exposure

Test bukan hanya unit test UI.

API projection test

it('does not include restricted fields without permission', async () => {
  const response = await api.getCase({ user: supportL1, caseId: 'CASE-123' });

  expect(response).not.toHaveProperty('customer.nationalId');
  expect(response).not.toHaveProperty('internalRiskScore');
  expect(response.customer.displayName).toBe('Jane D.');
});

DOM test

expect(screen.queryByText('1234567890')).not.toBeInTheDocument();
expect(document.body.innerHTML).not.toContain('1234567890');

Network payload test

expect(JSON.stringify(responseBody)).not.toContain('refresh_token');
expect(JSON.stringify(responseBody)).not.toContain('nationalId');

Error report test

const report = buildErrorReport(error, context);
expect(JSON.stringify(report)).not.toContain('jane@example.com');
expect(JSON.stringify(report)).not.toContain('Bearer ');

Query cache test

await logout();
expect(queryClient.getQueryCache().getAll()).toHaveLength(0);

29. Production observability

Track sensitive exposure controls without exposing sensitive data.

Useful metrics:

sensitive_reveal_clicked_total
sensitive_reveal_denied_total
sensitive_export_requested_total
sensitive_export_denied_total
analytics_blocked_on_sensitive_route_total
session_replay_disabled_route_total
redaction_applied_total
client_error_report_dropped_sensitive_context_total
cache_cleared_on_logout_total

Audit events:

type SensitiveRevealAuditEvent = {
  type: 'sensitive.reveal';
  actorId: string;
  tenantId: string;
  resourceType: 'case' | 'customer' | 'document';
  resourceId: string;
  fieldGroup: 'identity' | 'financial' | 'investigation' | 'medical';
  reason?: string;
  requestId: string;
  occurredAt: string;
};

Do not audit raw sensitive value.


30. Incident response model

Jika sensitive data masuk UI/log/analytics:

  1. Identify data class.
  2. Identify exposure surface.
  3. Determine affected tenants/users/resources.
  4. Stop further collection.
  5. Purge cache/log/vendor data if possible.
  6. Rotate credentials if secrets leaked.
  7. Revoke sessions if tokens/session identifiers leaked.
  8. Patch projection/serialization/logging.
  9. Add regression tests.
  10. Document timeline and audit evidence.

Sensitive exposure incident sering bukan “hack”; bisa berupa over-fetching atau analytics config. Dampaknya tetap nyata.


31. Anti-pattern catalog

// BAD: hide but still pass sensitive data
<SensitivePanel hidden={!canView} nationalId={customer.nationalId} />
// BAD: log arbitrary API response
console.log(await response.json());
// BAD: analytics with PII
analytics.track('case_view', { customerEmail: customer.email });
// BAD: error boundary dumps error object
<pre>{JSON.stringify(error)}</pre>
// BAD: persist sensitive query cache
persistQueryClient({ queryClient, persister: localStoragePersister });
// GOOD: minimal server projection
<UserCard displayName={projection.displayName} />
// GOOD: cache clear on logout
queryClient.clear();
// GOOD: safe analytics event
analytics.track('case_view', { routeGroup: 'case-detail' });

32. Final mental model

Sensitive data exposure in React is mostly a projection failure.

A weak engineer says:

We hide the field if user lacks permission.

A strong engineer asks:

Was the field ever sent to the browser?
Was it serialized into HTML?
Was it cached?
Was it logged?
Was it sent to analytics?
Was it present in DOM attributes?
Was it cleared on logout and tenant switch?
Could a browser extension, XSS, error reporter, session replay, or source map reveal it?

The invariant:

Authorization decides who may access data. Projection decides what data exists in the browser. Redaction reduces blast radius. Cache/log/analytics policy prevents secondary disclosure.

React UI is not just pixels. It is a data distribution surface.


References

Lesson Recap

You just completed lesson 86 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.