rules:
  - id: auth.flow.secret-in-response
    languages:
      - javascript
      - typescript
    severity: ERROR
    message: |
      A server-side secret read from `process.env` flows into an HTTP
      response body. Whatever you put in `res.send` / `res.json` / `res.end`
      ships straight to the client, so returning a credential here publishes
      it to every caller. It ends up in the browser, in proxies, and in any
      logged response (CWE-200, Sensitive Information Exposure).

      Never send a server secret to the client. Return only the data the
      caller needs; if the response must reference a credential, send a
      non-sensitive identifier (a key id, the last four characters) or a
      redacted/masked value instead. Keep API keys, passwords, tokens, and
      private keys server-side only.
    # Reverse-direction taint rule: the SOURCE is the secret (a `process.env.*`
    # value whose NAME looks like a credential) and the SINK is the Express
    # response body, the opposite of the request->sink flow rules (ssrf,
    # open-redirect). Taint mode catches indirection: `const k =
    # process.env.API_KEY; res.send(k)` flags, not just the inline form.
    #
    # Low-FP control is the source NAME regex: it requires a credential-shaped
    # name AND excludes the client-public prefixes (PUBLIC_, NEXT_PUBLIC_,
    # VITE_, REACT_APP_, EXPO_PUBLIC_) whose values are exposed to the browser
    # by design. Sending those back is not a leak. Distinct from
    # auth.secret.public-env-secret, which is a build-time *exposure* check on
    # the env-var name alone; this rule is a *runtime dataflow* into a response.
    mode: taint
    pattern-sources:
      # process.env.SECRET_NAME: member-access form.
      - patterns:
          - pattern: process.env.$KEY
          - metavariable-regex:
              metavariable: $KEY
              regex: (?i)^(?!(?:NEXT_PUBLIC|EXPO_PUBLIC|REACT_APP|PUBLIC|VITE)_).*(?:secret|password|passwd|token|api[_-]?key|private[_-]?key|client[_-]?secret|credential|access[_-]?key).*$
      # process.env['SECRET_NAME']: index form.
      - patterns:
          - pattern: process.env[$K]
          - metavariable-regex:
              metavariable: $K
              regex: (?i)^(?!(?:NEXT_PUBLIC|EXPO_PUBLIC|REACT_APP|PUBLIC|VITE)_).*(?:secret|password|passwd|token|api[_-]?key|private[_-]?key|client[_-]?secret|credential|access[_-]?key).*$
    pattern-sanitizers:
      # Routing the value through a redaction/masking helper clears the taint:
      # the masked/redacted form is no longer the live secret.
      - pattern: redact(...)
      - pattern: mask(...)
    pattern-sinks:
      - patterns:
          - pattern-either:
              - pattern: $RES.send($X)
              - pattern: $RES.json($X)
              - pattern: $RES.jsonp($X)
              - pattern: $RES.end($X)
              - pattern: $RES.write($X)
          - focus-metavariable: $X
    metadata:
      oauthlint-rule-id: AUTH-FLOW-012
      oauthlint-doc-url: https://oauthlint.dev/rules/flow-secret-in-response
      category: security
      cwe: CWE-200
      owasp: API3:2023
      llm-prevalence: HIGH
      technology:
        - express
      references:
        - https://cwe.mitre.org/data/definitions/200.html
        - https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/
