v0.14 is out: a mobile auth pack for Swift/iOS and Android, catching insecure token storage, cleartext traffic, and OAuth in embedded WebViews. Read more →
HIGH AI PREVALENCE: HIGH auth.flow.secret-in-response

A server-side secret read from process.env flows into an HTTP response body.

CWE-200 OWASP API3:2023 js · ts DATAFLOW

Why AI tools produce this: AI coding tools generate this anti-pattern by default, it appears in a large share of AI-written auth code.

Dataflow rule. This is a taint-mode rule: it traces a hardcoded secret, token or credential through your code to the HTTP response body, so indirection across multiple lines is caught, not just the direct one-line form. Routing the value through a recognised validation / allow-list sanitizer clears the taint and suppresses the finding. Why dataflow →

Why this matters

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.

VULNERABLE
vulnerable.ts
import type { Request, Response } from 'express';

// Inline: API key read from env and returned in a JSON body.
export function getConfig(_req: Request, res: Response): void {
  // ruleid: auth.flow.secret-in-response
  res.json({ apiKey: process.env.API_KEY });
}

// Intra-procedural dataflow: assign the secret to a local, then res.send() it.
export function leakPassword(_req: Request, res: Response): void {
  const s = process.env.DB_PASSWORD;
  // ruleid: auth.flow.secret-in-response
  res.send(s);
}

// OAuth client secret returned in JSON.
export function getClient(_req: Request, res: Response): void {
  // ruleid: auth.flow.secret-in-response
  res.json({ secret: process.env.CLIENT_SECRET });
}

// Token flows through a local into res.end().
export function dumpToken(_req: Request, res: Response): void {
  const t = process.env.ACCESS_TOKEN;
  // ruleid: auth.flow.secret-in-response
  res.end(t);
}

// Index-access form of process.env into res.jsonp().
export function getKey(_req: Request, res: Response): void {
  // ruleid: auth.flow.secret-in-response
  res.jsonp({ key: process.env['STRIPE_API_KEY'] });
}

// Private key streamed back via res.write().
export function writeCredential(_req: Request, res: Response): void {
  const pk = process.env.PRIVATE_KEY;
  // ruleid: auth.flow.secret-in-response
  res.write(pk);
  res.end();
}
SAFE
safe.ts
import type { Request, Response } from 'express';

declare function redact(value: string | undefined): string;

// Client-public var (NEXT_PUBLIC_*): exposed to the browser by design, no leak.
export function getPublicConfig(_req: Request, res: Response): void {
  res.json({ url: process.env.NEXT_PUBLIC_API_URL });
}

// Non-secret operational var: the name does not look like a credential.
export function getPort(_req: Request, res: Response): void {
  res.json({ port: process.env.PORT });
}

// A plain constant — never a secret source, never tainted.
export function getVersion(_req: Request, res: Response): void {
  const version = '1.0.0';
  res.send(version);
}

// Secret routed through a redaction helper before the response: taint cleared.
export function getMaskedKey(_req: Request, res: Response): void {
  res.json({ apiKey: redact(process.env.API_KEY) });
}

// Vite client-public var whose name DOES contain a credential keyword
// (api_key) but carries the VITE_ prefix — exposed by design, excluded by the
// source regex's negative lookahead, so no finding.
export function getViteConfig(_req: Request, res: Response): void {
  res.json({ key: process.env.VITE_API_KEY });
}

Suppressing this rule

If a finding is a genuine false positive, scope the suppression to the exact line and leave a reason, never disable the rule project-wide. Disable directives are line-scoped by design.

// oauthlint-disable-next-line auth.flow.secret-in-response -- <reason>

References

https://cwe.mitre.org/data/definitions/200.html ↗https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/ ↗