PROCYON

PRC005 — Response returns a secret

Severityhigh
OWASP API Security Top 10API3:2023 Broken Object Property Level Authorization

What it catches

A field in a response schema whose name says it holds a secret. Every schema an operation can return is checked: nested objects, array items, allOf/oneOf/anyOf, additionalProperties and $refs. A schema shared by several operations is reported once, where the property is written (see ignoring for ignores on one of those operations).

Two kinds of secret:

KindWords (English and Portuguese)Reported in
Never returnedpassword, passwd, passphrase, senha, secret, segredo, private key, chave privadaany response
Issued oncetoken, API key, access keyGET responses only

Tokens and API keys are how authentication works: a login (POST) returns a token, creating a key returns the key. What must never happen is reading one back later, which is what a GET returning it allows.

How names are matched

Names are split into words: userPassword, password_hash, api-token, APIKey and senhaUsuario become user password, password hash, api token, api key, senha usuario. A name matches when:

  • it contains a listed word whole, or its plural (apiKeys, tokens, senhas);
  • the word right before it does not make it a paging cursor or a page-bound token: nextPageToken, continuationToken, syncToken, csrfToken are not credentials;
  • the word right after it does not describe the secret instead of holding it: passwordPolicy, tokenExpiresAt, apiKeyId, secretName, secretNamespace, token_risk_score, secretScanning, passwordResetPolicyName, apiKeyPublic, senhaValidade. Only that next word counts, so senhaEmTextoPlano is still a password.

A secret is a value: text, a number, a list or a map of them (additionalProperties), or an object that wraps one in a value, data, plaintext or content field (password: {value: ...}). Not reported: booleans (passwordSet, and OpenAPI 3.1's type: [boolean, "null"]), which only say a secret exists; enums of several values (secrets: read | write, a permission), since a secret is not a choice (a one-value enum is reported: it documents a fixed secret); and other objects (secretRef: {name, key}, imagePullSecrets: [{name}], an anyOf of objects), which point at a secret or group fields checked on their own. A schema without a type counts. A property that is a $ref is reported where the property is written, not at the shared schema it refers to.

ReportedNot reported
userPassword, password_hash, passwordHash, tokenDatapasswordPolicy, passwordUpdatedAt
accessToken, api-token, APIKey, apiKeys (in a GET)tokenExpiresAt, tokenType, apiKeyId, nextPageToken
senhaUsuario, senhaEmTextoPlano, privateKey, clientSecretstokenizer, secretary (not the whole word)
passwordSet, hasPassword (booleans)
secretRef, imagePullSecrets (objects), secrets: read | write (an enum)
secretNamespace, token_reference_id, token_risk_score, secretScanning

Why it matters

Whoever can read the response gets the secret, and responses travel far: server and proxy logs, caches, browser developer tools, mobile apps, error trackers. A password hash can be cracked offline; a token read back from a listing lets one user act as another.

How to fix it

Remove the field from the response schema. Check passwords and secrets on the server, and return only what the client needs (for example hasPassword: true).

How to ignore it

If the finding is fine on purpose, write an x-procyon-ignore on the operation, with the reason. The finding is still reported, under Ignored, and no longer fails the run. See ignoring a finding.

/api-keys/{id}:
  get:
    x-procyon-ignore:
      - rule: PRC005
        reason: "The field holds the user's own key, shown once after creation"

Examples

Reported:

/users/{id}:
  get:
    responses:
      "200":
        content:
          application/json:
            schema:
              properties:
                passwordHash:
                  type: string

Not reported: hasPassword: {type: boolean} in its place, which only says a password exists.

Known limits

Matching is by name. A secret in a field called value is not found, and a non-secret field called secret (a book's "secret" chapter) is reported. Both are rare; report either as a false negative or false positive issue.