rules:
  - id: auth.go.flow.secret-in-response
    languages:
      - go
    severity: ERROR
    message: |
      A server-side secret read from the environment flows into an HTTP
      response body, leaking it to the client. Values such as an API key,
      client secret, access key, or private key are meant to stay on the
      server; writing one to the `http.ResponseWriter` (via `Write`,
      `fmt.Fprint(f)`, `io.WriteString`, or a JSON encoder) publishes it to
      every caller, including attackers probing your endpoints.

      Never return a credential to the client. Send only the data the caller
      legitimately needs; if a secret must appear in a debug/diagnostic path,
      redact or mask it first. Read secrets exclusively in server-internal
      code and keep them out of any response payload. See CWE-200.
    # Reverse-direction taint: the SOURCE is the secret (an os.Getenv /
    # os.LookupEnv read whose key name looks like a credential) and the SINK is
    # the HTTP response. Taint mode so indirection (s := os.Getenv("API_KEY");
    # w.Write([]byte(s))) is caught, not just the inline form. The taint is
    # cleared by a redaction/mask helper before the value reaches the response.
    mode: taint
    pattern-sources:
      # $KEY is the env-var name literal. The metavariable-regex keeps only
      # names that look like a credential, while a single negative lookahead
      # excludes client-public prefixes (PUBLIC_ / NEXT_PUBLIC_). Those ship to
      # the browser by design and are not server secrets.
      - patterns:
          - pattern-either:
              - pattern: os.Getenv($KEY)
              - pattern: os.LookupEnv($KEY)
          - metavariable-regex:
              metavariable: $KEY
              regex: (?i)^(?!"?(next_)?public_)"?.*(secret|password|passwd|token|api[_-]?key|private[_-]?key|client[_-]?secret|credential|access[_-]?key).*"?$
    pattern-sanitizers:
      # A redaction/mask helper clears the taint: the value written to the
      # response is no longer the raw secret.
      - pattern: redact(...)
      - pattern: mask(...)
    pattern-sinks:
      # Focus the value argument so the finding lands on the leaked secret, not
      # the whole call. Plain-call forms (no aliasing) match reliably in taint
      # mode. $W is the http.ResponseWriter.
      - patterns:
          - pattern-either:
              - pattern: $W.Write($X)
              - pattern: fmt.Fprint($W, $X)
              - pattern: fmt.Fprintf($W, $FMT, $X)
              - pattern: io.WriteString($W, $X)
              - pattern: json.NewEncoder($W).Encode($X)
          # The env-read SOURCE shapes are disjoint from these response sinks,
          # but exclude them explicitly so an env read is only ever a source,
          # never re-counted as a sink.
          - pattern-not: os.Getenv(...)
          - pattern-not: os.LookupEnv(...)
          - focus-metavariable: $X
    metadata:
      oauthlint-rule-id: AUTH-GO-FLOW-004
      oauthlint-doc-url: https://oauthlint.dev/rules/go-flow-secret-in-response
      category: security
      cwe: CWE-200
      owasp: API3:2023
      llm-prevalence: HIGH
      technology:
        - net/http
      references:
        - https://cwe.mitre.org/data/definitions/200.html
        - https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/
