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.oauth.open-redirect-callback

The OAuth callback handler redirects to a URL taken straight from the request without validating it.

CWE-601 OWASP API1: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 untrusted request input (query, body, params) through your code to an HTTP redirect destination, 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

An attacker can craft a phishing link to your real callback that forwards the victim to a malicious site under your domain's trust.

Maintain an explicit allow-list of post-login redirect destinations (route names or full URLs you control). Never forward to an arbitrary req.query.redirect_to, req.query.next, or req.query.return_url value.

VULNERABLE
vulnerable.ts
interface Req {
  query: { redirect_to?: string; next?: string; url?: string };
  body: { return_url?: string };
}
interface Res {
  redirect: ((url: string) => void) & ((code: number, url: string) => void);
}

export function badCallback(req: Req, res: Res) {
  // ruleid: auth.oauth.open-redirect-callback
  res.redirect(req.query.redirect_to as string);
}

export function badCallback2(req: Req, res: Res) {
  // ruleid: auth.oauth.open-redirect-callback
  res.redirect(req.body.return_url as string);
}

export function badCallback3(req: Req, res: Res) {
  const key = 'next';
  // ruleid: auth.oauth.open-redirect-callback
  res.redirect(req.query[key] as string);
}

export function badCallbackIndirect(req: Req, res: Res) {
  const next = req.query.next as string;
  // ruleid: auth.oauth.open-redirect-callback -- variable indirection
  res.redirect(next);
}

export function badCallbackDefault(req: Req, res: Res) {
  // ruleid: auth.oauth.open-redirect-callback -- logical-or default
  res.redirect((req.query.next as string) || '/');
}

export function badCallbackStatus(req: Req, res: Res) {
  // ruleid: auth.oauth.open-redirect-callback -- status + url overload
  res.redirect(302, req.query.url as string);
}
SAFE
safe.ts
interface Req {
  query: { next?: string };
}
interface Res {
  redirect: (url: string) => void;
}

// ok: auth.oauth.open-redirect-callback -- map the input to a controlled constant
const DESTINATIONS: Record<string, string> = {
  profile: '/profile',
  settings: '/settings',
};
export function goodCallback(req: Req, res: Res) {
  switch (req.query.next) {
    case 'profile':
      res.redirect(DESTINATIONS.profile);
      return;
    case 'settings':
      res.redirect(DESTINATIONS.settings);
      return;
    default:
      res.redirect('/dashboard');
  }
}

// ok: auth.oauth.open-redirect-callback -- always redirects to a fixed destination
export function goodCallback2(_req: Req, res: Res) {
  res.redirect('/dashboard');
}

const ALLOWED = new Set(['/profile', '/settings']);

// ok: auth.oauth.open-redirect-callback -- inline allow-list guard validates the
// value before it reaches res.redirect.
export function guardedCallback(req: Req, res: Res) {
  if (ALLOWED.has(req.query.next as string)) {
    res.redirect(req.query.next as string);
  }
}

const ALLOWED_ARR = ['/profile', '/settings'];

// ok: auth.oauth.open-redirect-callback -- Array.includes guard.
export function guardedCallbackIncludes(req: Req, res: Res) {
  if (ALLOWED_ARR.includes(req.query.next as string)) {
    res.redirect(req.query.next as string);
  }
}

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.oauth.open-redirect-callback -- <reason>

References

https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html ↗