PII Leak in Response (JavaScript / TypeScript)
ID |
pii_leak_in_response_jsts |
Severity |
high / critical (composite with |
Remediation Complexity |
medium |
Remediation Risk |
high |
Remediation Effort |
medium |
Family |
API3:2023 - Broken Object Property Level Authorization |
CWE |
CWE-359 |
Resource |
data_exposure |
Language |
Express, NestJS, Koa, Fastify, Hono |
Description
Reports endpoints whose response model carries fields tagged PII (Personally Identifiable Information), PCI (payment-card data), or PHI (protected health information) by the sensitivity classifier. Distinct from excessive_data_exposure in scope:
-
excessive_data_exposure— over-fetching, any sensitivity-tagged field the client didn’t ask for (including credentials). -
pii_leak_in_response— sensitive-data sinks. Any time PII / PCI / PHI reaches the wire, regardless of whether the client asked for it.
CREDENTIAL and CRYPTO_MATERIAL tags are intentionally excluded — login and OAuth endpoints legitimately return tokens / cookies, and treating those as PII leaks would drown the signal.
The detector pairs with unauthenticated_endpoint via the FlawCorrelator: when the same endpoint also has no authentication requirement, a composite CRITICAL pii_leak_in_unauthenticated_endpoint finding is emitted.
Rationale
PII / PCI / PHI in a response is not automatically a defect — a user fetching their own profile needs to see their email and phone. The risk is when the response carries those fields beyond the operation’s intended scope:
-
Cross-user leak —
/users/search?name=alicereturns full PII for every match. The endpoint’s purpose is "find a username"; returning email / phone / DOB / address is over-disclosure. -
Anonymous / partner endpoint leak — a B2B partner endpoint that returns a list of customers with full PII (or worse, PCI / PHI), where the partner’s contract entitles them to aggregate numbers only.
-
Unauthenticated leak — the worst case. Any unauthenticated PII / PCI / PHI sink is a critical breach — under GDPR Art-32 ("appropriate technical measures") and PCI-DSS 3.4 ("render PAN unreadable"), there’s no defensible "well, it’s TLS-encrypted" argument.
The regulatory side compounds the technical one: a single unauthenticated PII endpoint discovered by a journalist or competitor is a public disclosure event. Treat PII / PCI / PHI in responses as a default-deny shape and justify each exposure case by case.
A susceptible Express search route:
app.get('/users/search', async (req, res) => {
const users = await User.find({
username: { $regex: req.query.q, $options: 'i' },
});
res.json(users); // each user has email, phone, dob, address — PII
});
User.find(…) returns the full Mongoose documents; res.json serialises every field.
Remediation
Three approaches, in order of robustness:
-
Remove the field from the response DTO if the operation doesn’t genuinely require it. The fastest fix; defaults to least disclosure.
-
Mask or redact at the response boundary. Show
j***@example.cominstead ofjohn.doe@example.com, last-four-digits of a phone number, the first letter of a name. Pair with full unmasked access from a separate, audited "details" endpoint. -
Tighten authorization on the endpoint. If the field is necessary, confirm the caller is entitled to it — pair this finding with a BOLA / BFLA review. An admin reading any user’s email is acceptable; a logged-in non-admin reading any user’s email is not.
Cross-cutting:
-
If the sensitivity tag is wrong for your project — e.g., an email-newsletter app where
emailis intentionally public — override the tag rather than suppressing the finding. -
Pair this detector with
unauthenticated_endpointreview. The composite CRITICAL finding is the actionable one.
A revised route with explicit projection and shaping:
const toSummary = (u) => ({ id: u.id, username: u.username });
app.get('/users/search', async (req, res) => {
const users = await User
.find({ username: { $regex: req.query.q, $options: 'i' } }, 'username')
.lean();
res.json(users.map(toSummary));
});
The Mongoose projection 'username' selects only the needed column at the database layer, and toSummary is the single place where the wire shape is defined.
NestJS equivalent: a dedicated UserSummaryDto with @Expose() annotations, returned via ClassSerializerInterceptor with strategy: 'excludeAll'.
Configuration
The detector accepts minConfidence (default high) and inherits the sensitivity classifier’s global configuration. The set of tags considered "PII" (PII / PCI / PHI) is fixed; CREDENTIAL and CRYPTO_MATERIAL are intentionally excluded from this detector’s scope.
References
-
OWASP API Security Top 10 (2023) - API3:2023 - Broken Object Property Level Authorization.
-
OWASP API Security Top 10 (2023) - API10:2023 - Unsafe Consumption of APIs.
-
CWE-359 : Exposure of Private Personal Information to an Unauthorized Actor.
-
OWASP Cheat Sheets Series: User Privacy Protection Cheat Sheet.
-
GDPR Article 32 — Security of processing.
-
PCI-DSS v4.0 — Requirement 3.4 (render PAN unreadable wherever it is stored, processed, or transmitted).