PII Leak in Response (Go)

ID

pii_leak_in_response_go

Severity

high / critical (composite with unauthenticated_endpoint)

Remediation Complexity

medium

Remediation Risk

high

Remediation Effort

medium

Family

API3:2023 - Broken Object Property Level Authorization

CWE

CWE-359

Resource

data_exposure

Language

net/http, Gin, Echo, Chi, Fiber

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=alice returns 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 Gin / GORM search handler:

type User struct {
    ID          int64
    Username    string
    Email       string  // PII
    Phone       string  // PII
    DateOfBirth string  // PII
    Address     string  // PII
}

func SearchUsers(c *gin.Context) {
    var users []User
    db.Where("username LIKE ?", "%"+c.Query("q")+"%").Find(&users)
    c.JSON(http.StatusOK, users)
}

Find(&users) populates the full struct; c.JSON marshals every exported field — including the PII columns.

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.com instead of john.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 email is intentionally public — override the tag rather than suppressing the finding.

  • Pair this detector with unauthenticated_endpoint review. The composite CRITICAL finding is the actionable one.

A revised handler with an explicit summary struct:

type UserSummary struct {
    ID       int64  `json:"id"`
    Username string `json:"username"`
}

func SearchUsers(c *gin.Context) {
    var summaries []UserSummary
    db.Model(&User{}).
        Select("id", "username").
        Where("username LIKE ?", "%"+c.Query("q")+"%").
        Find(&summaries)
    c.JSON(http.StatusOK, summaries)
}

GORM’s Select("id", "username") mirrors the wire shape on the database side; PII columns never leave the table.

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