Positive security for the edge

Allow only what your code accepts.

Positive Edge reads your Next.js app with Claude Opus 5.5, writes a contract for every route — each header, cookie, param and body field — and enforces it on Cloudflare Workers. Everything else is denied.

$ uses: edwardwc/positive-edge@main
edge · enforceacme.workers.dev
MethodPathCode
  • GET/api/users/42200
  • POST/api/legacy403
  • DELETE/api/users/42405
  • GET/api/users/42200
  • POST/api/upload200
  • GET/api/users/1%2f..%2fadmin403
  • GET/?q=shoes&utm_source=x200
  • PATCH/api/users/42403

Default

deny

Signatures

0

Generated by

Claude Opus 5.5

WAF · negative model

  • Blocks patterns someone already wrote a rule for.
  • Every new CVE is a race against signature updates.
  • Forwards every header, cookie and field not recognised as bad.

Positive Edge · positive model

  • Allows only inputs your code demonstrably reads.
  • Unknown attacks fail closed. There is no list to get ahead of.
  • The origin never sees headers, cookies or fields it doesn't use.

How it works

Your codebase is the specification.

01

Read the code

A GitHub Action discovers every App Router, Pages Router, server action and middleware path, and gathers the exact source each one runs.

02

Derive the contract

Claude Opus 5.5 reads each handler and writes the narrowest rule the code justifies, with file and line evidence.

03

Review as a PR

edge-contract.json lands as a pull request with a diff and per-route confidence. Security is reviewed like code.

04

Enforce at the edge

A Cloudflare Worker validates every request before your origin sees it. Start in report mode, then enforce.

The contract

Every rule traces back to a line of code.

A real route and the contract Claude Opus 5.5 generated for it. The auth regex, the integer id and the strict PATCH body are lifted from the handler.

app/api/users/[id]/route.ts
// app/api/users/[id]/route.ts
const patchSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email().max(254),
}).strict();

function isAuthorized(value: string | null) {
  return value !== null && /^Bearer [A-Za-z0-9._-]{1,64}$/u.test(value);
}

export async function PATCH(request: Request, { params }) {
  const { id } = await params;
  if (!isAuthorized((await headers()).get("authorization"))) return unauthorized();
  const userId = Number(id);
  if (!Number.isSafeInteger(userId) || userId < 1) return badRequest();
  const parsed = patchSchema.safeParse(await request.json());
  …
}
edge-contract.json
{
  "id": "api-api-users-by-id",
  "path": "/api/users/:id",
  "methods": ["GET", "PATCH"],
  "params": {
    "id": { "type": "integer", "minimum": 1, "pattern": "^[1-9][0-9]{0,15}$" }
  },
  "headers": {
    "authorization": { "required": true, "pattern": "^Bearer [A-Za-z0-9._-]{1,64}$" }
  },
  "unknown": { "query": "deny", "headers": "strip", "cookies": "strip" },
  "methodRules": {
    "PATCH": { "body": {
      "contentTypes": ["application/json"], "maxBytes": 2048,
      "json": { "type": "object", "required": ["name", "email"],
        "properties": {
          "name":  { "type": "string", "minLength": 1, "maxLength": 100 },
          "email": { "type": "string", "format": "email", "maxLength": 254 }
        }, "additionalProperties": false }
    } }
  },
  "confidence": "high"
}

Bodies

JSON Schema 2020-12 with additionalProperties: false forced on. Duplicate keys, prototype keys and deep nesting rejected.

Headers

Only headers the code reads are forwarded. Spoofed x-forwarded-* and framework-internal headers never reach the origin.

Cookies

Each cookie has its own type, pattern and length. Unknown cookies are stripped.

Paths

Typed :params and catch-alls with deterministic precedence. Traversal and ambiguous encodings rejected before routing.

Uploads

Per-field rules for multipart and urlencoded forms: file counts, byte limits and MIME types.

Report mode

Ship observe-only first. Violations stream to the dashboard without touching traffic.

CVE-2025-29927 · critical

Blocked without knowing it existed.

On Next.js 14.2.24 one x-middleware-subrequest header skips middleware — including auth. The generated contract never allowed that header and required the session cookie middleware checks. Two independent layers, no signature.

RequestNext.js directEdge
GET /admin307 → /login403
GET /admin cookie: session200200
GET /admin x-middleware-subrequest: middleware×5200 bypassed403

FAQ

Questions

Your app already knows what it accepts. Make the edge know too.