DEV Community

Cover image for Add a real WAF to your FastAPI, Express, or Next.js app in one line
OBI EBUKA DAVID
OBI EBUKA DAVID

Posted on

Add a real WAF to your FastAPI, Express, or Next.js app in one line

The first time an app of mine got popped, it was not a clever attack. Someone changed a number in a URL. /api/orders/8123 became /api/orders/8124, and they were looking at a stranger's order. No SQL injection, no payload, nothing my WAF had ever been trained to recognize. Just a well-formed GET request for an object that was not theirs.

That is the part nobody tells you when you buy a WAF. The WAF you are paying for was built to catch a different decade's attacks.

What your WAF is actually doing

A traditional WAF is a signature engine. It carries a list of known-bad patterns, most of them descended from the OWASP Core Rule Set, and it matches every request against that list. SQL injection strings, cross-site scripting payloads, path traversal, a few thousand regexes. If a request matches, it blocks. If it does not, it passes.

This works great for the noise. Automated scanners hammering your login page with ' OR 1=1 get stopped cold, and that is worth something.

But look at what it cannot see. The order-number attack I described has no bad pattern in it. It is a legitimate request shape hitting a legitimate route with a legitimate session. The only thing wrong is that this user should not have access to that object, and a signature engine has no idea what any user should have access to. Broken object-level authorization sits at the very top of the OWASP API Security Top 10 for a reason. It is the most common way APIs get breached, and it is invisible to the model your WAF is built on.

Same story for broken auth, for business-logic abuse, for the endpoint you shipped last Tuesday that leaks a little more than it should. None of it looks like an attack. All of it is an attack.

The other way to do this

There is a different model, and it is old enough that the idea is not controversial: instead of listing everything bad and blocking that, list everything good and block the rest. Allow-list instead of deny-list. In security terms it is called positive security.

The reason nobody did it for web apps is that writing the allow-list by hand is miserable. Your app has hundreds of routes, each with its own request shapes, auth requirements, and access patterns, and they change every sprint. Nobody is going to maintain that by hand, and they were right not to try.

The thing that changed is that you no longer have to write it. You let the app teach you what normal looks like. Every route it actually serves, the shape of the requests that hit it, whether the caller was authenticated, what a real user's access pattern looks like. Learn that, and then anything that deviates from it gets flagged. The order-number attack deviates, because this user has never accessed that object and the access pattern does not fit. It gets caught for what it is, not because someone wrote a rule.

That is what I have been building with Nemesis Shield, so the rest of this is how you actually wire it up. It is genuinely one line, and it is free to start.

FastAPI

Install the package and add the middleware. That is the whole integration.

pip install nemesis-shield
Enter fullscreen mode Exit fullscreen mode
from fastapi import FastAPI
from nemesis_shield.asgi import SentinelMiddleware

app = FastAPI()
app.add_middleware(SentinelMiddleware, token="nsk_your_app_token")
Enter fullscreen mode Exit fullscreen mode

Your routes do not change. Your handlers do not change. The middleware sits in front, watches the request and response shapes, and reports them.

Express

npm install @nemesis-shield-autogon/sentinel
Enter fullscreen mode Exit fullscreen mode
const express = require("express");
const { sentinel } = require("@nemesis-shield-autogon/sentinel/express");

const app = express();
app.use(sentinel({ token: process.env.NEMESIS_TOKEN }));
Enter fullscreen mode Exit fullscreen mode

Register it before your routes so it sees every request, including the ones that match no handler. Fastify and Koa have the same one-liner if that is your stack.

Next.js

For Next, guard everything from the middleware:

// middleware.ts
import { withShield } from "@nemesis-shield-autogon/edge";
import { NextResponse } from "next/server";

export const config = { matcher: "/:path*" };
export default withShield(() => NextResponse.next(), {
  token: process.env.NEMESIS_TOKEN,
});
Enter fullscreen mode Exit fullscreen mode

The same withShield wrapper works for Vercel Edge, Cloudflare Workers, and Supabase Edge Functions, which matters more than it sounds like, because those are usually the parts of an app that have no protection at all.

The part that keeps you from getting paged

Here is the thing that makes this safe to ship, and the thing I would have wanted to hear before turning any WAF loose on production traffic.

It starts in observe mode. Blocks nothing. It watches real traffic and builds the baseline, and everything it sees shows up in a review queue with the evidence attached. You look at what it learned, you approve it, and only then do you flip it to enforce. There is no moment where you deploy a wall and pray it does not brick your checkout flow.

If you do not want to wait on real traffic to fill out the baseline, point the Nemesis Learn agent at your app in staging and it exercises your routes for you:

npx @nemesis-shield-autogon/learn \
  --target http://localhost:3000 \
  --app-token nsk_your_app_token \
  --repo .
Enter fullscreen mode Exit fullscreen mode

It reads your repo to find routes, drives them over HTTP, and reports coverage, so you can see exactly when the baseline is ready to enforce instead of guessing.

And it is fail-open. If the Nemesis service is ever unreachable, the SDK gets out of the way and your app serves the request. A security tool that can take your app down when its own backend has a bad day is not a security tool, it is a second outage waiting to happen. I have watched that movie. The SDK ships behavioral shapes only, the method, the shape of the path, the status, whether the caller was authenticated. Not your request bodies, not your secrets, not your source.

Where this fits, honestly

Positive security is not a replacement for writing correct authorization code. If your handler does not check that the order belongs to the user, fix the handler. The WAF is the layer that catches the mistake you did not know you made, and the zero-day in a dependency you cannot patch until Tuesday, and the endpoint a teammate shipped without telling you. It buys you time and it catches the class of attack a signature engine was never going to see. That is the whole pitch.

It also needs a clean learning window. If you first turn it loose on an app that is already under active attack, it will learn the attack as normal, so do the learning in staging or during a quiet period and approve what it shows you. That is the one real gotcha, and observe-first exists precisely so you never skip it.

If you are building with an AI editor, you do not even have to add the line yourself. The Nemesis MCP server gives your coding agent the tools to do it while it writes the code:

npx -y @nemesis-shield-autogon/mcp
Enter fullscreen mode Exit fullscreen mode

Ask it to protect your app and it installs the SDK, runs the learn agent, and walks the baseline to enforce for you.

The free tier covers one app and does not need a card, which is enough to protect the side project that is quietly holding real user data. Change one number in one of your own URLs sometime and see what comes back. If it is somebody else's data, you already know which layer was supposed to stop that, and you already know it did not.

Top comments (0)