<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>ApiTraffic Blog</title>
    <link>https://dev.apitraffic.io/blog</link>
    <description>API observability, APM and application security for Node.js teams.</description>
    <language>en-us</language>
    <lastBuildDate>Fri, 30 Oct 2026 21:52:58 GMT</lastBuildDate>
    <atom:link href="https://dev.apitraffic.io/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>AI Didn&apos;t Invent New API Attacks. It Made the Old Ones Cheap.</title>
      <link>https://dev.apitraffic.io/blog/ai-made-api-attacks-cheap</link>
      <guid isPermaLink="true">https://dev.apitraffic.io/blog/ai-made-api-attacks-cheap</guid>
      <pubDate>Thu, 01 Oct 2026 13:00:00 GMT</pubDate>
      <description>Enumeration, credential stuffing and mass assignment are decade-old techniques. What changed is that an AI agent will now run all of them against your Node API in ten minutes, without getting bored. Here&apos;s what that looks like, and where to start.</description>
      <content:encoded><![CDATA[<p>Enumeration, credential stuffing, scraping and injection are not new. Most of them are older than the frameworks you’re running them against.</p>
<p>What changed is who runs them. An AI agent will try every ID, every parameter and every forgotten route at 2 a.m., without getting bored, for the cost of a few API credits. The basics you meant to get to later are now the whole attack surface.</p>
<p>The numbers back that up:</p>
<ul>
<li><strong>53%</strong> of all web traffic in 2025 was automated. Humans are now the minority.</li>
<li><strong>27%</strong> of bot attacks in 2025 went straight at API endpoints, not web pages.</li>
<li>AI-driven bot attacks grew <strong>12.5×</strong> from 2024 to 2025.</li>
<li>In a large study of 100 models, <strong>45%</strong> of coding tasks introduced a security flaw. Your copilot writes fast; it doesn’t write safe by default.</li>
</ul>
<p>The danger isn’t magic. It’s volume and speed aimed at the gaps every small team has: missing ownership checks, no rate limits, no logs.</p>
<h2 id="ten-minutes-one-agent">Ten minutes, one agent <a class="post-heading-anchor" href="#ten-minutes-one-agent" aria-hidden="true">#</a></h2>
<p>Here’s what an AI-driven probe looks like in your request logs. It’s an illustrative composite of common patterns, not real customer traffic.</p>
<div class="post-table"><table>
<thead>
<tr>
<th>Time</th>
<th>Request</th>
<th>Status</th>
<th>What happened</th>
</tr>
</thead>
<tbody>
<tr>
<td>02:14:07</td>
<td><code>GET /.env /.git/config /actuator</code></td>
<td>404</td>
<td>Recon from one IP</td>
</tr>
<tr>
<td>02:14:08</td>
<td><code>GET /api/v1/openapi.json</code></td>
<td>200</td>
<td>Full route map, handed over</td>
</tr>
<tr>
<td>02:14:09</td>
<td><code>POST /api/v1/signup</code></td>
<td>201</td>
<td>A fresh account</td>
</tr>
<tr>
<td>02:14:11</td>
<td><code>GET /api/v1/invoices/10418 … 11203</code></td>
<td>200</td>
<td>785 invoices that weren’t theirs</td>
</tr>
<tr>
<td>02:16:30</td>
<td><code>POST /api/v1/login</code></td>
<td>401</td>
<td>1,240 attempts across 312 IPs</td>
</tr>
<tr>
<td>02:19:44</td>
<td><code>PATCH /api/v1/users/me {"role":"admin"}</code></td>
<td>200</td>
<td>Mass assignment</td>
</tr>
<tr>
<td>02:21:02</td>
<td><code>POST /api/v1/chat "ignore previous…"</code></td>
<td>200</td>
<td>Prompt injection</td>
</tr>
<tr>
<td>02:23:15</td>
<td><code>POST /api/v1/coupons/redeem</code></td>
<td>200</td>
<td>64 redemptions, one account</td>
</tr>
<tr>
<td>02:24:50</td>
<td><code>GET /api/v1/export?limit=100000</code></td>
<td>500</td>
<td>Stack trace leaked</td>
</tr>
</tbody>
</table></div>
<p>Every line is a decade-old technique. An agent just runs them all in a row.</p>
<p>And that’s the version where you have request logs. Without them, this is what you’re at risk for, and you’d have no record it happened.</p>
<h2 id="five-checks-that-stop-most-of-it">Five checks that stop most of it <a class="post-heading-anchor" href="#five-checks-that-stop-most-of-it" aria-hidden="true">#</a></h2>
<p>The full checklist has 40 items. These five close the worst gaps in the log above.</p>
<h3 id="1-scope-every-object-lookup-to-the-caller">1. Scope every object lookup to the caller <a class="post-heading-anchor" href="#1-scope-every-object-lookup-to-the-caller" aria-hidden="true">#</a></h3>
<p>Broken object-level authorization is #1 on the OWASP API Security Top 10 because it’s one missing <code>WHERE</code> clause, and agents are built to find it. The agent signs up, fetches its own invoice, sees <code>/invoices/10418</code>, and asks for <code>10419</code>. Then the next 784. A human gets bored at ten. The agent doesn’t.</p>
<div class="post-code"><div class="post-code-head"><span class="post-code-label">js</span></div><pre><code class="language-js hljs">app.<span class="hljs-title">get</span>(<span class="hljs-string">'/api/invoices/:id'</span>, requireAuth, <span class="hljs-title">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">const</span> invoice = <span class="hljs-keyword">await</span> db.<span class="hljs-property">invoice</span>.<span class="hljs-title">findFirst</span>({
    <span class="hljs-attr">where</span>: { <span class="hljs-attr">id</span>: req.<span class="hljs-property">params</span>.<span class="hljs-property">id</span>, <span class="hljs-attr">accountId</span>: req.<span class="hljs-property">user</span>.<span class="hljs-property">accountId</span> }, <span class="hljs-comment">// ownership, every time</span>
  });
  <span class="hljs-keyword">if</span> (!invoice) <span class="hljs-keyword">return</span> res.<span class="hljs-title">status</span>(<span class="hljs-number">404</span>).<span class="hljs-title">end</span>();
  res.<span class="hljs-title">json</span>(<span class="hljs-title">toPublicInvoice</span>(invoice)); <span class="hljs-comment">// allowlisted fields only</span>
});
</code></pre></div>
<p>If the row isn’t theirs, it doesn’t exist. Return <code>404</code>, not <code>403</code>, so you don’t confirm it’s there.</p>
<h3 id="2-rate-limit-login-on-the-account-not-just-the-ip">2. Rate limit login on the account, not just the IP <a class="post-heading-anchor" href="#2-rate-limit-login-on-the-account-not-just-the-ip" aria-hidden="true">#</a></h3>
<p>Per-IP limits fail against rotating proxies. In the log above, 1,240 login attempts came from 312 IPs, so no single IP looked busy. Key the limit on the targeted account too.</p>
<div class="post-code"><div class="post-code-head"><span class="post-code-label">js</span></div><pre><code class="language-js hljs"><span class="hljs-keyword">import</span> { rateLimit, ipKeyGenerator } <span class="hljs-keyword">from</span> <span class="hljs-string">'express-rate-limit'</span>;

app.<span class="hljs-title">post</span>(<span class="hljs-string">'/api/login'</span>, express.<span class="hljs-title">json</span>(), <span class="hljs-title">rateLimit</span>({
  <span class="hljs-attr">windowMs</span>: <span class="hljs-number">15</span> * <span class="hljs-number">60_000</span>,
  <span class="hljs-attr">limit</span>: <span class="hljs-number">10</span>,
  <span class="hljs-attr">standardHeaders</span>: <span class="hljs-string">'draft-8'</span>,
  <span class="hljs-attr">legacyHeaders</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">keyGenerator</span>: <span class="hljs-function">(<span class="hljs-params">req</span>) =&gt;</span> <span class="hljs-string">`<span class="hljs-subst">${ipKeyGenerator(req.ip)}</span>:<span class="hljs-subst">${req.body?.email ?? <span class="hljs-string">''</span>}</span>`</span>,
}), login);
</code></pre></div>
<h3 id="3-reject-fields-you-didnt-ask-for">3. Reject fields you didn’t ask for <a class="post-heading-anchor" href="#3-reject-fields-you-didnt-ask-for" aria-hidden="true">#</a></h3>
<p>Mass assignment turns any profile update into a promotion. AI-generated handlers love <code>data: req.body</code>. A strict schema rejects unknown keys before they ever reach the database.</p>
<div class="post-code"><div class="post-code-head"><span class="post-code-label">js</span></div><pre><code class="language-js hljs"><span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;

<span class="hljs-keyword">const</span> <span class="hljs-title">UpdateProfile</span> = z.<span class="hljs-title">strictObject</span>({ <span class="hljs-comment">// unknown keys → 400</span>
  <span class="hljs-attr">name</span>: z.<span class="hljs-title">string</span>().<span class="hljs-title">trim</span>().<span class="hljs-title">min</span>(<span class="hljs-number">1</span>).<span class="hljs-title">max</span>(<span class="hljs-number">80</span>),
  <span class="hljs-attr">timezone</span>: z.<span class="hljs-title">string</span>().<span class="hljs-title">max</span>(<span class="hljs-number">64</span>).<span class="hljs-title">optional</span>(),
});

app.<span class="hljs-title">patch</span>(<span class="hljs-string">'/api/users/me'</span>, express.<span class="hljs-title">json</span>({ <span class="hljs-attr">limit</span>: <span class="hljs-string">'100kb'</span> }), <span class="hljs-title">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">const</span> parsed = <span class="hljs-title">UpdateProfile</span>.<span class="hljs-title">safeParse</span>(req.<span class="hljs-property">body</span>);
  <span class="hljs-keyword">if</span> (!parsed.<span class="hljs-property">success</span>) <span class="hljs-keyword">return</span> res.<span class="hljs-title">status</span>(<span class="hljs-number">400</span>).<span class="hljs-title">json</span>({ <span class="hljs-attr">error</span>: <span class="hljs-string">'invalid_body'</span>, <span class="hljs-attr">requestId</span>: req.<span class="hljs-property">id</span> });
  <span class="hljs-keyword">await</span> db.<span class="hljs-property">user</span>.<span class="hljs-title">update</span>({ <span class="hljs-attr">where</span>: { <span class="hljs-attr">id</span>: req.<span class="hljs-property">user</span>.<span class="hljs-property">id</span> }, <span class="hljs-attr">data</span>: parsed.<span class="hljs-property">data</span> });
  res.<span class="hljs-title">status</span>(<span class="hljs-number">204</span>).<span class="hljs-title">end</span>(); <span class="hljs-comment">// {"role":"admin"} never got this far</span>
});
</code></pre></div>
<h3 id="4-fail-quietly">4. Fail quietly <a class="post-heading-anchor" href="#4-fail-quietly" aria-hidden="true">#</a></h3>
<p>A verbose 500 is a free architecture diagram: your ORM, your table names, your file paths. In production, return an error code and a request ID. Log the detail; send the ID. And cap the things that make 500s easy to trigger: body size, page size and export size.</p>
<h3 id="5-treat-your-ai-feature-like-the-api-it-is">5. Treat your AI feature like the API it is <a class="post-heading-anchor" href="#5-treat-your-ai-feature-like-the-api-it-is" aria-hidden="true">#</a></h3>
<p>A chat box or an agent with tools is a new endpoint that takes free text and acts on it. You can’t fully prevent prompt injection. You can make sure an injected model can only do what that user already could:</p>
<ul>
<li>Run every tool call with the end user’s permissions, never a service account’s.</li>
<li>Set per-user token, request and spend budgets. An open chat endpoint is a free model for anyone who finds it, billed to you.</li>
<li>Require a human click for anything irreversible: refunds, deletes, sends, payments.</li>
</ul>
<h2 id="the-one-that-unlocks-the-rest">The one that unlocks the rest <a class="post-heading-anchor" href="#the-one-that-unlocks-the-rest" aria-hidden="true">#</a></h2>
<p>Alerts, enumeration detection, prompt forensics and incident scope all depend on one thing: knowing what’s hitting your API. If you only do one thing today, start logging every request, with secrets redacted before they’re stored.</p>
<h2 id="get-the-full-40-point-checklist">Get the full 40-point checklist <a class="post-heading-anchor" href="#get-the-full-40-point-checklist" aria-hidden="true">#</a></h2>
<p>The AI-Era API Security Checklist covers all 40 checks across 9 areas, each prioritized and mapped to the OWASP API Security Top 10 and the OWASP Top 10 for LLM Apps. It also includes:</p>
<ul>
<li><strong>5 Node snippets</strong> you can paste today</li>
<li><strong>A 30-day plan</strong> for a team of one to twenty</li>
<li><strong>An AI audit prompt.</strong> Paste it into Claude Code, Cursor or Copilot from your repo root, and it audits your app against all 40 items and writes a full report with file-and-line evidence.</li>
</ul>
<p>Attackers point agents at your API. Point one at your code first.</p>
<p><strong><a href="/resources/ai-era-api-security-checklist">Get the free checklist →</a></strong></p>
<hr />
<p><em>Sources: Thales/Imperva, 2026 Bad Bot Report; Asia Pacific Defence Reporter on the same Thales report; Veracode, 2025 GenAI Code Security Report; OWASP API Security Top 10 (2023); OWASP Top 10 for LLM Applications (2025). Code samples target express-rate-limit 8 and zod 4 and are starting points, not drop-in security.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Sample Post</title>
      <link>https://dev.apitraffic.io/blog/sample-post</link>
      <guid isPermaLink="true">https://dev.apitraffic.io/blog/sample-post</guid>
      <pubDate>Mon, 10 Aug 2026 18:03:20 GMT</pubDate>
      <description>This is a sample</description>
      <content:encoded><![CDATA[<p>This is a sample blog post</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
