SAST scanner · reads middleware.ts

Middleware Analyzer

Next.js middleware only runs on the paths its matcher names. This analyzer lays that list over the API routes found in the repository and reports each route left outside, then checks whether the middleware really verifies a token and whether its matcher can be sidestepped.

SASTup to HighAuth Weakness4 checksNext.js

Matcher vs route tree

The scanner holds two lists and finds the difference. On the left is what the middleware says it covers. On the right is what the repository actually exposes.

middleware.ts export const config = { matcher: ["/dashboard/:path*", "/api/tasks/:path*", "/api/:path*"], ], } each string becomes a regular expression ^/dashboard(/.*)?$ ^/api/tasks(/.*)?$ ^/api(/.*)?$ route_map: every API route in the repository /api/tasksapp/api/tasks/route.tscovered /api/tasks/:idapp/api/tasks/[id]/route.tscovered /api/admin/usersapp/api/admin/users/route.tsnot covered · Medium 0.85covered by /api/:path* /api/auth/loginapp/api/auth/login/route.tsnot covered · Medium 0.85covered by /api/:path* /api/healthapp/api/health/route.tsnot covered · Medium 0.85covered by /api/:path*

1A Next.js middleware.ts runs only on the paths its matcher lists. The scanner holds two things: that list, and the API routes found under app/api and pages/api. The question is which routes the list leaves out.

2Each matcher string is converted to a regular expression. :path* becomes an optional slash followed by anything, so /api/tasks/:path* reads as ^/api/tasks(/.*)?$.

3/api/tasks and /api/tasks/:id both match the second pattern. They are covered, and the middleware's auth check runs for them.

4/api/admin/users, /api/auth/login and /api/health match none of the patterns. Each uncovered route is its own Medium finding, 0.85. The scanner has no public-path list, so the intentionally public login and health routes are reported as well.

5Add /api/:path* and every route is covered. The public ones are then excluded inside the middleware function with an explicit allow-list, which is the shape the markdown recommends.

Matcher shapes

The same file can name its matcher in three styles. Each one changes what is covered and which extra findings appear.

matcher stringhow it is readwhat comes out "/api/:path*" ^/api(/.*)?$regular expression, tried on /api/tasks covers /api and everything below itno bypass findings: it ends in * "/admin" ^/admin$literal path, matches /admin only covers exactly /adminLow 0.70: /admin/ is not mentionedLow 0.70: mixed case may slip past "/((?!api|_next/static).*)" ((?!api|_next/static).*)tried on the route without its first slash /api/tasks is tested as api/tasksthe lookahead rejects it: not covered, Mediumregex matchers get no bypass checks

1A matcher can be written three ways, and each is read differently. The same conversion applies to the config of any Next.js app.

2The common :path* form becomes a prefix pattern. It covers the path itself and everything below it, and its trailing * exempts it from the bypass checks.

3A literal path matches that path only. The scanner adds two Low findings, because a matcher with no * and no trailing slash may miss /admin/, and a literal lowercase path may miss a differently cased request.

4A regular-expression matcher is run on the route without its leading slash. The popular (?!api|_next/static) form excludes /api on purpose, so every API route lands outside the middleware. That is the pattern the markdown warns about.

What it reports

Four stages run in order on the middleware file. Each has its own severity, so the report separates a missing file from a leaky matcher.

1 Find the filemiddleware.ts, middleware.js,src/middleware.ts, src/middleware.js HIGH · 0.90 No auth middleware foundneeds API routes, not Express 2 Matcher vs routesmatcher regexes against everyAPI route in route_map MEDIUM · 0.85 API route not coveredone finding per uncovered route 3 Read the token checkweak: the cookie existsstrong: a token is verified MEDIUM · 0.75 Weak authentication checkweak present and no strong 4 Test matcher shapesno trailing slash, caseskips regex matchers LOW · 0.70 Matcher may be bypassableper literal matcher If no file is found the scan ends with one High finding, provided the repository has API routes. An Express backend (a middleware/ directory using express or req, res, next) suppresses it: the Express analyzer owns that case. Matcher strings are read from export const config = { matcher: [...] }. A route is covered if any pattern matches it. With no matcher found there is nothing to compare, so this stage is skipped. weak patterns: request.cookies.has( cookies().has( a request.cookies.get(...) at the end of a line strong patterns: getUser( getSession( auth() verifyToken( getToken( createServerClient updateSession and more A finding needs a weak pattern and no strong one anywhere in the file, so one real verification clears it. A literal matcher such as "/admin" gets two Low findings: no trailing-slash handling, and a case-sensitive path. Matchers ending in * or containing :path* are not flagged for the trailing slash; regex-style matchers are skipped.

1No middleware file in the repository is the broadest gap. High, 0.90, but only when there are API routes to protect and the repo is not an Express app.

2The coverage comparison from the first figure. Medium, 0.85 per route, so a matcher that misses ten routes produces ten findings.

3The middleware body is read for a token check. A check that only asks whether a cookie exists is Medium, 0.75: an attacker can send any cookie value. Any strong pattern in the file clears it.

4Last, the shape of each literal matcher. Low, 0.70, as a prompt to confirm that slashes and case are handled. These are the weakest findings in the group.

Why it matters

Why it matters

Middleware is often the first line of defence, and a matcher decides where that line stands. If it protects page routes but not API routes, an attacker skips the pages and calls the API directly: the browser reaches /dashboard through the check, curl reaches /api/tasks without it.

Real-world breach

Optus (2022). A breach that exposed 9.8 million records was caused by an API endpoint with no authentication. The markdown notes that the middleware, or its equivalent, protected user-facing pages but not the API, which is the pattern this scanner looks for.

Vulnerable vs fixed code

// BAD: pages are covered, API routes are not
export const config = {
  matcher: ["/dashboard/:path*"]
}

export function middleware(request: NextRequest) {
  // weak: only asks whether a cookie exists
  if (request.cookies.has("session")) return NextResponse.next()
  return NextResponse.redirect(new URL("/login", request.url))
}
Scope note

The markdown example reads the Authorization header and rejects a request that has none. That proves a header exists, not that its token is valid. The analyzer does not recognise that form as a strong check, because its header pattern is lowercase and case-sensitive. Use a verification call such as getUser, getToken or verifyToken.

Limits and scope

BehaviourDetail
Inputmiddleware.ts or .js, at the repo root or in src/, plus the route map. No network.
Routes comparedAPI routes only (app/api/**/route.ts and pages/api/**). A page such as /dashboard is never in the list, so a matcher that misses a page is not reported.
Severity and confidenceNo middleware: High 0.90. Uncovered route: Medium 0.85 each. Weak token check: Medium 0.75. Matcher bypass: Low 0.70 each.
Wildcards understoodOnly the /:name* form. A :path+ or :path? segment is compared as literal text.
Public routesNo allow-list. Login, register and health routes outside the matcher are reported like any other.
Strong token checkAny of getUser(, getSession(, auth(), verifyToken(, cookies().get, getToken(, createServerClient and a few others. Note that cookies().get reads a cookie and counts as strong.