← 全部新闻
security3 分钟阅读

Your AI-scaffolded API uses sequential IDs, and that's an authorization bug

作者:Seaworthy · 2026年8月30日

Lines of code displayed on a laptop screen

Photo by Rahul Mishra on Unsplash

Ask an AI assistant to scaffold a REST API for orders, documents, or invoices, and you'll get something close to this:

app.get('/api/orders/:id', async (req, res) => {
  const order = await db.query('SELECT * FROM orders WHERE id = ?', [req.params.id])
  res.json(order)
})

It works. You test it with order 42, it comes back, you move on. Nothing here checks whether the order at id 42 belongs to the person making the request, and nothing in the response, the test, or the code itself hints that a check is missing.

The bug is the missing question, not the missing code

This is an insecure direct object reference. The endpoint trusts that if you can name a resource, you're allowed to see it. IDs generated by an auto-incrementing primary key are trivial to name: order 42 implies order 41 and order 43 exist too, and a script that walks the integer range will find them all. No credentials to steal, no injection payload to craft. Just a for loop.

What makes this so common in AI-generated code specifically is that the scaffold is functionally correct. It fetches the row, it returns JSON, it matches the route the prompt asked for. An authorization check isn't a feature the endpoint is missing, it's a question the endpoint never asked: does the current user own this? Nothing about the happy-path prompt ("add a GET endpoint for orders by id") implies that question needs asking, so a model optimizing for a working demo won't add it unprompted.

Fix the query, not just the handler

app.get('/api/orders/:id', requireAuth, async (req, res) => {
  const order = await db.query(
    'SELECT * FROM orders WHERE id = ? AND user_id = ?',
    [req.params.id, req.user.id]
  )
  if (!order) return res.status(404).json({ error: 'Not found' })
  res.json(order)
})

The ownership check belongs in the query, not as a separate if (order.userId !== req.user.id) after the fetch. A query scoped to the current user returns nothing for someone else's order, which you then correctly report as 404. A fetch-then-check pattern often leaks the distinction between "doesn't exist" and "exists but isn't yours" through timing, error messages, or a lazy 403 that confirms the resource is real. Returning 404 for both cases is deliberate: it tells an attacker nothing about which order IDs are valid.

UUIDs help, but they're not the fix

Switching from sequential integers to UUIDs is a common reaction, and it does remove the ability to enumerate resources by guessing nearby IDs. It does not add an authorization check. A UUID that leaks in a shared link, a browser history, or a referrer header is just as readable by the wrong person as an integer, and an API that never checks ownership will happily return the resource to whoever holds the string. Unguessable IDs raise the cost of finding a resource. They don't decide who's allowed to have it.

Every resource-by-id route needs this same question answered

The same missing check shows up wherever a route takes an id from the URL and a query runs off it directly: PATCH /api/documents/:id, DELETE /api/invoices/:id, GET /api/users/:id/settings. Each one needs the same answer to the same question, scoped consistently, not reinvented per route as a bespoke if statement that's easy to forget on the next one someone adds.

Seaworthy flags database queries built from a route parameter that never join against the authenticated user's identity.

本文由 AI 生成,总结了公开可用的来源。

来自 Seaworthy 团队

Seaworthy 会扫描你的代码仓库,发现类似本文提到的各种问题。

安全漏洞、泄露的密钥和配置错误——在部署前捕获。免费运行,无需账户。

$ npx seaworthycode