APIs do not ship with the defaults you want. They ship with the defaults that make the tutorial work. CORS wide open so the hello-world demo runs. An internal admin handler exposed on the same router as the public one. A default credential on a management dashboard nobody remembered to turn off. None of this is code. It is settings.
Security misconfiguration is when the bug is not in your code, it is in your settings. CORS wildcards, exposed admin routes, default credentials, debug endpoints in production, management dashboards on public ports. The code compiles. The app works. The settings are wrong.
What your AI actually built
You asked for an API with authentication and a dashboard. The model gave you the API, the dashboard, and — without mentioning it — a CORS handler set to Access-Control-Allow-Origin: * with credentials enabled, because that is what the getting-started guides do.
It also wired up a few internal endpoints next to the public ones: /api/admin/users for the dashboard, /api/internal/queue for the worker, /api/debug/env for the build you did at 2am. None of them have auth, because the model assumed they would only ever be called 'from inside.' They are on the same public router.
And somewhere in your compose file there is a management service — pgAdmin, Redis Commander, a queue dashboard — that the model added because you said 'I want to see the data.' It is bound to 0.0.0.0 with the default credentials baked into the image.
How it gets exploited
The attacker fingerprints the app, scrapes the Next.js build manifest, and lists every route the client knows about.
- 1Find the extrasAmong the user-facing routes are /api/admin/users, /api/internal/queue, and /api/debug/env. None of them were meant to be public. All of them respond.
- 2Walk the debug route/api/debug/env returns the process env as JSON. Inside: a Stripe live key, a Resend API key, the database URL, and the NextAuth secret.
- 3Cross-origin the sessionThe app allows Access-Control-Allow-Origin: * with credentials. The attacker hosts a page that fetches /api/me from the victim's browser and reads the session data — no XSS needed.
- 4Log into the admin toolA scan of the box finds a Redis Commander on port 8081 with admin/admin. Full read and write on the session store.
None of these were bugs in the code. They were bugs in the config. Each one on its own would have been ugly. Together, they are a complete compromise of the app — and the code-review tool has nothing to say about any of them.
Vulnerable vs Fixed
// app/api/_middleware.ts
export function middleware(req: Request) {
const res = NextResponse.next();
res.headers.set('Access-Control-Allow-Origin', '*');
res.headers.set('Access-Control-Allow-Credentials', 'true');
return res;
}
// app/api/admin/users/route.ts
export async function GET() {
// "Internal only — nobody will call this from outside"
return Response.json(await db.user.findMany());
}
// app/api/debug/env/route.ts
export async function GET() {
return Response.json(process.env);
}// app/api/_middleware.ts
const ALLOWED = new Set([
'https://app.example.com',
'https://www.example.com',
]);
export function middleware(req: Request) {
const origin = req.headers.get('origin') ?? '';
const res = NextResponse.next();
if (ALLOWED.has(origin)) {
res.headers.set('Access-Control-Allow-Origin', origin);
res.headers.set('Access-Control-Allow-Credentials', 'true');
res.headers.set('Vary', 'Origin');
}
return res;
}
// app/api/admin/users/route.ts
export async function GET(req: Request) {
const session = await requireAdmin(req); // throws 403 otherwise
return Response.json(await db.user.findMany());
}
// app/api/debug/env/route.ts — deleted in prod builds
export async function GET() {
if (process.env.NODE_ENV === 'production') {
return new Response('Not found', { status: 404 });
}
return Response.json({ env: 'dev only' });
}Three rules. CORS is an allowlist of exact origins, never a wildcard with credentials. Every route on a public router requires auth — there is no such thing as 'internal only' on an endpoint anyone on the internet can reach. Debug endpoints do not exist in production builds. Delete them or gate them on NODE_ENV.
A real case
Exposed management consoles keep ending up in search results
Every year, researchers find hundreds of production Kibana, Redis Commander, and Elasticsearch dashboards bound to 0.0.0.0 with default credentials — discovered by simple Shodan queries, not exploits.
Related reading
What we find
security misconfigurationReferences
Find the settings that are wrong.
Flowpatrol checks every route, every CORS header, and every management port for the defaults that should have been turned off. Five minutes. One URL.
Try it free