PII Leak in Response (PHP)
ID |
pii_leak_in_response_php |
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 |
Laravel, Symfony, Slim |
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 Laravel search route:
// routes/api.php
Route::get('/users/search', function (Request $request) {
return User::where('username', 'like', '%' . $request->q . '%')->get();
});
// User has email, phone, date_of_birth, address — all PII
→get() returns full Eloquent models; the implicit toArray() serialises every column.
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 an API Resource collection:
// app/Http/Resources/UserSummaryResource.php
class UserSummaryResource extends JsonResource {
public function toArray($request): array {
return [
'id' => $this->id,
'username' => $this->username,
];
}
}
// routes/api.php
Route::get('/users/search', function (Request $request) {
$users = User::where('username', 'like', '%' . $request->q . '%')
->select(['id', 'username'])
->get();
return UserSummaryResource::collection($users);
});
select(['id', 'username']) narrows the SQL projection; UserSummaryResource declares the wire shape. PII columns never leave the database.
A separate, properly-authorised /users/{user} route returns the full record when the caller is entitled to it.
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).