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.