Read-only support access stops an agent from changing a customer’s account. It doesn’t stop them seeing everything in it. Sensitive data masking and field-level minimisation decide what support can see. Read-only scope decides what they can do. A safe support session needs both.
Think about what a read-only session can open without writing anything: an integrations page that shows the customer’s API key, a billing page with a full address and payment details, a message inbox, a CSV export. Each one is a read. The read-only impersonation guide covers blocking writes and dangerous operations. This guide covers the other half: what support sees, and how to limit it.
Why doesn’t read-only access protect sensitive data?
Read-only is about state changes, not disclosure. A read-only rule asks “does this request change anything?” A sensitive-data rule asks “should this person see this value?” Those are different questions, and most systems only answer the first.
OWASP’s API Security Top 10 (opens in a new tab) describes the underlying problem. APIs tend to return every property of an object, and an endpoint is vulnerable when it exposes properties the user shouldn’t read. In an impersonation session there’s a twist: your API is behaving correctly. The customer is allowed to see their own address and settings. The person looking just isn’t the customer.
That’s why support access needs its own minimisation rules. GDPR’s data protection by default (opens in a new tab) expects only the personal data necessary for each purpose to be processed, and says the obligation covers its accessibility. A support ticket about a broken export button rarely needs a customer’s date of birth.
Where does sensitive data leak in a read-only session?
Most leaks come from ordinary reads that nobody thought of as sensitive:
| Surface | What can leak | Why read-only misses it | Better control |
|---|---|---|---|
| Settings and integrations | API keys, webhook secrets, OAuth tokens | Viewing settings is a GET | Show secrets once, then only a prefix or name |
| Profile and billing | Addresses, phone numbers, payment and government identifiers | Viewing a profile is a GET | Mask on the server for support sessions |
| Search and lists | Other users’ records in the tenant, message previews | Search is a read, sometimes a POST | Limit fields in search results |
| Exports and downloads | Whole datasets, invoices, attachments, signed file links | Exports change nothing in the database | Block, or require a separate reviewed path |
| Messages and documents | Private user content | Content is just data | Hide by default; reveal per ticket with a reason |
| Logs, recordings and tickets | Anything shown on screen | Copies are made after the response | Mask capture; keep values out of tickets |
Secrets deserve special handling. Stripe, for example, displays a live secret key (opens in a new tab) you create one time, and you can’t reveal it later. If your settings page can’t return a full secret to anyone, it can’t return one to support either.
Exports are easy to miss because they look harmless. OWASP’s logging guidance (opens in a new tab) lists data import and export, including screen-based reports, among the events you should always log, alongside access to sensitive data.
What is sensitive data masking, and where should it happen?
Sensitive data masking is replacing or removing a value so the viewer can’t recover it. NIST defines masking (opens in a new tab) as removing a field or replacing it in a way that doesn’t keep the value’s analytic utility, such as swapping a phone number for asterisks.
For support access, masking has to happen on the server, before the response leaves your API. A blurred field in the UI still sits unmasked in the network response, the browser’s developer tools and any client-side cache.
| Approach | How it works | Choose it when | Watch out for |
|---|---|---|---|
| Field-level minimisation | Each endpoint returns only named properties | Always, as the baseline | Generic serializers that dump whole objects |
| Server-side masking for support sessions | The serializer masks named fields when the request is impersonated | Support needs the real UI with fewer values | Masking keyed on browser-supplied flags |
| Dedicated support view | Separate endpoints and screens built for support | Most tickets are about status and configuration | It can drift from what the customer sees |
| Column-level database grants | An internal tool’s database role can’t select sensitive columns | Internal tools query the database directly | Doesn’t help inside the customer’s own UI |
| Frontend-only masking | The page hides values the API still returns | Never as a control | The data is still in the response |
OWASP’s prevention advice (opens in a new tab) matches the first row: pick the specific properties you return rather than serializing whole objects, and keep returned data to the minimum the endpoint needs. For internal tools, PostgreSQL lets you grant SELECT on specific columns (opens in a new tab) rather than a whole table.
The second row needs a trustworthy signal. Key masking on verified impersonation context from your authentication layer, never on a header or body field the browser sends. In Devora integrations, for example, the backend receives verified context (opens in a new tab) that includes whether the request is impersonated, the actor, the subject and the scope.
Should support use a support view or live account access?
Use a support view for routine questions and live account access when the problem only shows up as the customer sees it. Many teams need both.
| Support view | Live account access | |
|---|---|---|
| What support sees | Status, configuration, recent errors, chosen summaries | The customer’s actual screens |
| Default data exposure | Low, by design | Everything the customer can see, unless you mask it |
| Catches UI and permission bugs | Rarely | Yes |
| Engineering effort | A separate set of screens to maintain | Session handling, scope enforcement and masking |
| Good for | ”Is my sync running?”, “Which plan am I on?" | "This button is missing for me”, “This report looks wrong” |
Choose a support view when the answer lives in metadata: plan, feature flags, job status, error codes, timestamps. That’s most tickets.
Choose live account access when you need to reproduce a problem as the customer, such as a layout, a permission or data-dependent behaviour. Combine it with server-side masking so the session shows the shape of the data without every value.
How do you decide which fields to mask?
Classify your data once, then set a default for support sessions per class. Here’s a reasonable starting point:
| Data class | Examples | Default in a support session |
|---|---|---|
| Secrets | API keys, webhook secrets, tokens, recovery codes | Never returned |
| Regulated identifiers | Government IDs, full payment card or bank numbers, health data | Masked; never revealed in support |
| Contact details | Email, phone, address | Partially masked |
| Customer content | Messages, documents, notes, uploads | Hidden; revealed per ticket with a reason |
| Operational metadata | Plan, status, flags, job results, error codes | Shown |
The “reveal with a reason” pattern works well for content. The agent asks to see a specific field or document, gives a reason, and your application records an audit event with the actor, the customer, the field name and the reason, never the value itself.
What about logs, recordings and screenshots?
Anything support sees can be copied somewhere less protected. Three places matter most.
Application logs. OWASP recommends (opens in a new tab) that access tokens, passwords, sensitive personal data and payment data are removed, masked, hashed or encrypted rather than logged directly. Don’t log full response bodies from support sessions “for debugging”.
Session recordings. Masking in a recording protects the replay, not the live view. In Devora, recording is off by default (opens in a new tab). When it’s on, the default profile is fully masked, sensitive inputs such as passwords, emails, phone numbers and card fields stay masked in every profile, and masking runs in the agent’s browser. The agent still sees the live page exactly as your app renders it. Whether to record at all is a separate decision, so make it deliberately rather than leaving recording on for every session.
Tickets and chat. Screenshots pasted into a help desk outlive the session. Set a team rule: describe the state, don’t paste the data.
How does Devora fit, and what stays your job?
Devora’s scope controls actions (opens in a new tab), not field visibility. Endpoint rules match a method and a path. Read scope allows GET, HEAD and OPTIONS by default, and always-blocked rules apply in both read and write sessions. A rule can cover every method on a route, as in Devora’s recommended * /api/api-keys/**, so you can block key and export routes even for reads.
Devora doesn’t rewrite or redact your API responses. Masking fields inside a live session is your application’s job, keyed on the verified context your backend receives.
The access workflow itself is minimised. When an agent searches for a customer, Devora drops attribute keys (opens in a new tab) that look like secrets or sensitive identifiers, such as ones containing ssn, card, iban or dob. A request keeps a snapshot of only the attributes the template showed. Your search handler should still return only what your team needs to pick the right user.
How do you test what support can see?
Test the network responses, not the screen. Use a synthetic customer with fake secrets and personal data, open a read-only support session, and inspect every response body. These are pass conditions to check, not results we’ve run.
| Check | Pass condition |
|---|---|
| Open settings and integrations in a support session | No full API key, webhook secret or token in any response body |
| Open the profile and billing pages | Identifiers and payment details arrive masked from the server |
| Make the same requests as the customer, outside a support session | The customer still sees their normal data |
| Send a fake “impersonated: false” header during a support session | Masking still applies, because it uses verified context |
| Run global search | Results contain only the fields on your support allowlist |
| Call the export or bulk download endpoint | Rejected; no file, job or signed link is created |
| Open an attachment link captured before the session | Link expired or rejected |
| Reveal a hidden document with a reason | Audit event has actor, customer, field and reason, not the content |
| Replay a recorded session that opened the billing page | Sensitive inputs and listed regions are masked |
| Search application logs for the session | No tokens, secrets or full payloads |
Start with the data classification table, because every other control depends on it. Then decide which tickets a support view can answer, and mask the rest of the live session on the server. When you’re ready to try the access side, create a Devora account and try one staging session against this matrix.
Sources
- OWASP API Security Top 10 (2023): API3 Broken Object Property Level Authorization (opens in a new tab)
- EUR-Lex: Regulation (EU) 2016/679 (GDPR), Article 25 (opens in a new tab)
- Stripe Docs: API keys (opens in a new tab)
- OWASP Logging Cheat Sheet (opens in a new tab)
- NIST CSRC Glossary: masking (opens in a new tab)
- PostgreSQL Documentation: Privileges (opens in a new tab)
- Devora: Scope and security (opens in a new tab)
- Devora: Recording privacy and activity logs (opens in a new tab)
- Devora: Search results and templates (opens in a new tab)