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: MEDIUM auth.flow.password-plaintext

A user-supplied password is being persisted WITHOUT being hashed first.

Why AI tools produce this: AI coding tools produce this regularly, typically when prompted for a shortcut or a quick fix.

Why this matters

Storing plaintext passwords means any database read (backup, SQL injection, misconfigured backup, contractor with read-only access) leaks every credential in one shot.

Hash with argon2id (recommended), bcrypt, or scrypt before persisting. Never use plain SHA-256, MD5, or any unsalted hash for passwords.

OWASP ASVS V2.4 mandates an adaptive, salted hash. Every modern stack ships one. There is no reason to roll your own.

VULNERABLE
vulnerable.ts
declare const db: {
  users: {
    create: (data: unknown) => Promise<void>;
    insert: (data: unknown) => Promise<void>;
  };
};

declare class User {
  constructor(data: unknown);
  static create(data: unknown): Promise<void>;
}

interface Req {
  body: { email: string; password: string };
}

export async function signupBad(req: Req) {
  // ruleid: auth.flow.password-plaintext
  await db.users.create({
    email: req.body.email,
    password: req.body.password,
  });
}

export async function signupBad2(req: Req) {
  // ruleid: auth.flow.password-plaintext
  await User.create({ email: req.body.email, password: req.body.password });
}

export async function signupBad3(req: Req) {
  // ruleid: auth.flow.password-plaintext
  const u = new User({ email: req.body.email, password: req.body.password });
  await db.users.insert(u);
}

declare const prisma: { user: { create: (args: unknown) => Promise<void> } };
declare const userRepo: { save: (data: unknown) => Promise<void> };

export async function signupBad4(req: Req) {
  // ruleid: auth.flow.password-plaintext -- Prisma nested data wrapper
  await prisma.user.create({ data: { email: req.body.email, password: req.body.password } });
}

export async function signupBad5(req: Req) {
  // ruleid: auth.flow.password-plaintext -- repository .save()
  await userRepo.save({ email: req.body.email, password: req.body.password });
}
SAFE
safe.ts
import argon2 from 'argon2';

declare const db: {
  users: { create: (data: unknown) => Promise<void> };
};

interface Req {
  body: { email: string; password: string };
}

// ok: auth.flow.password-plaintext
export async function signupGood(req: Req) {
  const hashedPassword = await argon2.hash(req.body.password);
  await db.users.create({ email: req.body.email, password: hashedPassword });
}

// ok: auth.flow.password-plaintext
export async function signupGood2(req: Req) {
  await db.users.create({
    email: req.body.email,
    password: await argon2.hash(req.body.password),
  });
}

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.password-plaintext -- <reason>

References

https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html ↗https://cwe.mitre.org/data/definitions/256.html ↗