VibeSecurity

Incidents and lessons

McHire 123456 Password and IDOR: Lessons for AI-Built Apps

In June 2025, researchers Ian Carroll and Sam Curry logged into an administration interface for McHire, the McDonald's hiring chatbot platform built by Paradox.ai, with the username 123456 and the password 123456. From there, a broken authorization check on an API let them read other applicants' records by changing a number. This post is for founders and indie builders whose apps were generated by Lovable, Bolt, Cursor, Replit or Claude Code and who have a login page, an admin panel and a table of user records. After reading it you will know the two mistakes that combined here, why generated CRUD code is prone to both, how to run the two-account test, and how to enforce ownership in Supabase RLS and in an Express or Next.js route.

By the VibeSecurity team10 min read

What the researchers found

Ian Carroll's write-up describes McHire as a chatbot recruitment platform used by a large share of McDonald's franchisees and built by Paradox.ai. The researchers found that the administration interface accepted the default credentials 123456 as username and 123456 as password, which gave them access to a restaurant management dashboard.

The second finding was an insecure direct object reference. Carroll names the endpoint as PUT /api/lead/cem-xhr with a lead_id parameter. The API did not check whether the caller was allowed to see that lead, so decrementing the number returned other applicants' data. Bleeping Computer reported that the researchers' own test application had a lead_id around 64,185,742, which is where the headline figure comes from.

The write-up's own summary is that together the two bugs allowed the researchers, and anyone else with a McHire account and access to any inbox, to retrieve the personal data of more than 64 million applicants. The data reachable included names, email addresses, phone numbers, addresses, candidacy state, shift preferences, form inputs and authentication tokens that could be used for impersonation.

The 64 million figure, and its qualifier

Be precise about what this number is. The researchers' claim is about reachability: the id space went up to around 64 million, and the API would return any id you asked for. It is not a count of records that were downloaded, and Bleeping Computer updated its own headline to clarify that the figure represents applications on the chatbot rather than unique applicants.

Paradox.ai's public statement gives a much smaller number for what was actually accessed. It says the researchers viewed five candidate records containing names, email addresses, phone numbers and IP addresses, plus two chat records with no candidate information, and that based on its records the test account was not accessed by any third party other than the researchers. Krebs on Security carried the same claim from the company.

Both statements can be true at once. The researchers stopped at a handful of records, as responsible testers do, and the company reports that handful. The exposure, meaning what an attacker could have reached, is the larger number. When you describe this incident, say roughly 64 million records were reachable and five were reportedly accessed, and you will be stating what the sources actually support.

How the disclosure and fix played out

The timeline in Carroll's write-up is short. The researchers disclosed to Paradox.ai and McDonald's on June 30, 2025 at 5:46 PM. The default credentials were disabled at 7:31 PM the same day, and Paradox.ai confirmed the issue resolved on July 1 at 10:18 PM. Bleeping Computer reported that McDonald's acknowledged the report within an hour.

Paradox.ai's statement, published July 9, describes the account as a legacy test account with an outdated password that had not been logged into since 2019 and, in the company's words, should have been decommissioned. It says the company revoked the credentials, patched the endpoint within a few hours, updated its password standards, launched a security contact address at security@paradox.ai and started a bug bounty program. McDonald's, as quoted by Bleeping Computer, called it an unacceptable vulnerability from a third-party provider and said it mandated immediate remediation.

Krebs on Security added a separate, reported finding: a Paradox developer's machine had been compromised by infostealer malware around the same period, exposing credentials and session cookies. That is a different failure from the two bugs above, but it is a reminder that a vendor's security posture is more than one login page.

Why AI-generated CRUD apps make both mistakes

Default credentials happen because generated apps need a way in on day one. The assistant seeds an admin user with a memorable password so you can log in and see the dashboard, and that seed script runs in production too because nothing separates the environments. Paradox.ai's account of a test account from 2019 that was never decommissioned is the same story at a bigger company: a convenience login that outlived its purpose.

IDOR happens because generated code treats a record id as sufficient proof. A typical generated route reads the id from the URL, fetches the row, and returns it. Authentication is checked, because the assistant was told to add login, but authorization is not, because nobody asked whether user A should be able to fetch user B's row. The page works in the preview, where you are the only user, so the gap never shows.

Sequential integer ids make this worse. If ids count upward, an attacker can walk the entire table with a loop. Random ids slow that down but do not fix it, because ids leak in URLs, emails and shared links. The fix is an ownership check, not a harder-to-guess id.

The two-account test you can run today

  1. 1Create two ordinary user accounts in your app, A and B. Do not use your admin account.
  2. 2As A, create a record of every kind your app stores: a profile, an order, an upload, a message. Note each record's id from the URL or the network tab in the browser's developer tools.
  3. 3Log out, log in as B, and request each of A's records by id: paste the URL, or replay the API call from the network tab with B's session token.
  4. 4Try every method, not just GET. Replay a PUT or PATCH with A's id and B's token. The McHire bug was on a PUT endpoint.
  5. 5Try the same requests with no session at all, and with a deleted or disabled account.
  6. 6Anything that returns A's data or changes A's record is an IDOR. Fix the ownership check, then re-run the test before you ship.

Enforcing ownership in Supabase with RLS

If your app talks to Supabase from the browser, the ownership check belongs in a Row Level Security policy, because the client can call any table the public key can reach. The pattern is to store the owner's auth.uid() on every row and compare it in the policy for each operation. With RLS enabled and only these policies present, user B asking for user A's row gets an empty result, whatever the client code does.

SQL: owner-only policies on an applications table
alter table public.applications enable row level security;

create policy "applicants read own rows"
  on public.applications for select
  using (auth.uid() = user_id);

create policy "applicants insert own rows"
  on public.applications for insert
  with check (auth.uid() = user_id);

create policy "applicants update own rows"
  on public.applications for update
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

Enforcing ownership in an Express or Next.js route

If your API sits in front of the database, the same rule applies in code: never fetch by id alone. Fetch by id and owner together, so a record that exists but belongs to someone else looks identical to a record that does not exist. Return 404 for both, which avoids confirming that the id is valid. The example is a Next.js App Router handler; an Express handler is the same shape with req.params and res.status.

TypeScript: ownership check in a Next.js route handler
import { NextResponse } from "next/server";
import { getSession } from "@/lib/auth";
import { db } from "@/lib/db";

export async function PUT(req: Request, { params }: { params: { id: string } }) {
  const session = await getSession(req);
  if (!session) return NextResponse.json({ error: "unauthorized" }, { status: 401 });

  const body = await req.json();
  const updated = await db.application.updateMany({
    where: { id: params.id, userId: session.userId },
    data: { shiftPreference: body.shiftPreference },
  });

  if (updated.count === 0) return NextResponse.json({ error: "not found" }, { status: 404 });
  return NextResponse.json({ ok: true });
}

Default and test credentials: a pre-launch sweep

The credential half of this incident is easier to fix and easier to forget. Before every launch, search your repository and seed scripts for the words admin, test, password and 123456, and for any user created by a migration. Every such account should be deleted in production or forced onto a generated password with multi-factor authentication. Then add a rule that no account may be created by a script in production without a documented owner and an expiry date.

Where default credentials hide in generated apps
LocationWhat to look forWhat to do
Seed and migration scriptsinsert into users with a literal password, createUser calls with admin@example.comDelete the account in production; keep seeds for local only
Environment files and examplesADMIN_PASSWORD=, DEFAULT_USER=, values copied from .env.exampleGenerate real secrets per environment; never reuse the example values
Admin panels and dashboardsA login that works with the same value for username and passwordForce a password reset and enable MFA for every admin
Vendor and partner test accountsAccounts created for demos, integrations or support, never revokedList them, assign an owner, set an expiry, and disable on schedule
Old staff and contractor loginsAccounts with no login for monthsDisable anything unused for 90 days and review quarterly

Running a responsible disclosure contact

The researchers in this case had to find someone to tell, and the company's own statement says it created a dedicated security address afterwards. You can do that before you need it. Publish a security.txt file at /.well-known/security.txt with a contact address, a preferred language and a link to a short policy page. Reply to every report within a business day, even if only to say you are looking. Say what you will not do, which is take legal action against good-faith research, and what you ask for, which is no access beyond what proves the bug and no public disclosure before a fix.

When a report arrives, act in the order the McHire timeline shows: disable the credential or endpoint first, confirm the fix, then investigate scope from your logs, then publish what you found. Paradox.ai's statement is a reasonable model of the last step, because it says what was accessed, by whom, what was changed and how to reach the company. A read-only external scan such as VibeSecurity can catch exposed admin panels and open endpoints before a researcher does, but the contact page is what turns a finding into a fix instead of a headline.

Frequently asked questions

What was the McHire 123456 password incident?

In June 2025, researchers Ian Carroll and Sam Curry found that an administration interface for McHire, the McDonald's hiring chatbot built by Paradox.ai, accepted 123456 as both username and password. Combined with an API that did not check record ownership, this let them reach other applicants' chat records and personal data by changing a numeric id.

Were 64 million McDonald's applicants actually breached?

The researchers reported that more than 64 million applicant records were reachable through the vulnerable API, based on the size of the id space. Paradox.ai says only five candidate records were viewed, by the researchers, and that no third party accessed the test account. Bleeping Computer clarified the figure counts applications, not unique people.

What is an IDOR vulnerability?

An insecure direct object reference is when an application accepts an identifier, such as a record id in a URL or request body, and returns or modifies that record without checking that the caller is allowed to access it. Attackers change the id to reach other users' data. The fix is an ownership check on every read and write, in the database policy or the server route.

How do I test my app for IDOR?

Create two normal user accounts. As the first, create records and note their ids. As the second, request those ids directly, replaying GET, PUT, PATCH and DELETE calls from the browser's network tab with the second account's session. Any response that returns or changes the first user's data is an IDOR that needs an ownership check.

How do I prevent default credentials shipping to production?

Search seeds, migrations and environment examples for literal passwords and admin accounts before each launch, and delete or reset them in production. Force multi-factor authentication for every admin, give every test or vendor account an owner and an expiry, and disable any account unused for 90 days. Keep seed scripts for local development only.

Put it into practice

Sources

  1. 1.Ian Carroll: McDonald's McHire research write-up
  2. 2.Bleeping Computer: '123456' password exposed chats for 64 million McDonald's job chatbot applications (July 11, 2025)
  3. 3.Paradox.ai: Responsible Security Update (July 9, 2025)
  4. 4.Krebs on Security: Poor Passwords Tattle on AI Hiring Bot Maker Paradox.ai (July 17, 2025)