Guide · Frontend
Sentry's September 2026 Next.js Flaw Explained: Why Middleware-Only Auth Fails
20 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- The @sentry/nextjs vulnerability, public ID CVE-2026-114139, let some requests skip your Next.js middleware, the code that runs before a page loads and often checks logins.
- It needs four things at once. You set Sentry's tunnelRoute option, which relays error reports through your own server. You build with webpack, one of the two bundlers Next.js can use. A page sits under the tunnel path, and middleware is your only login check.
- Upgrade to 9.47.2 or 10.75.2, both out in September 2026; the CVE was listed in October. Version 11 drops the skip, so a strict middleware must let tunnel requests through itself.
- Turbopack builds, the new default bundler in Next.js 16, are not affected, unless you wrap middleware by hand with Sentry's helper. Neither is a page that checks the session itself.
- Beginners can stop after 'What should you do now'. Engineers get our tests on Next.js 16.4.3: 10.75.1 served a private page, renaming the tunnel did not help, and some matcher exclusions leave the same gap.
What goes wrong when middleware is your only login check?
If middleware is your only login check, anything that stops it running makes your private pages public. That is what the @sentry/nextjs vulnerability did for some Next.js apps between 2025 and September 2026. Middleware, the code Next.js runs before a request reaches a page, was quietly skipped.
Picture a common setup. A Next.js app serves English and Hindi pages from a folder named app/[locale], so addresses look like /en/account and /hi/account. A middleware file checks the login cookie and returns 401 Unauthorised when it is missing. The setup is made up, but the code path is real, and we ran it.
The team also uses Sentry, an error-monitoring service, and turns on its tunnelRoute option with the path /monitoring. Many ad blockers drop requests to Sentry's servers. The tunnel sends browser error reports to the app's own server first, which then forwards them to Sentry.
Now someone with no cookie opens /monitoring/account. Next.js reads monitoring as the locale, finds the account page and renders it. On an affected webpack build, the middleware never runs, so the private page comes back with status 200.
Which words do you need before reading this advisory?
You need five terms. Each one comes with the part of the running example it maps to.
- Middleware: a function in middleware.ts that Next.js runs before routing each request. Next.js 16 renames the file to proxy.ts and deprecates the old name. In our example it checks the login cookie.
- Tunnel route: a path on your own site, here /monitoring, that receives Sentry error reports from the browser. Sentry's tunnelRoute option sets it up for you.
- Rewrite: a Next.js rule that serves one address from another without changing the browser's address bar. Sentry adds a rewrite that forwards tunnel requests to its ingest servers.
- Dynamic segment: a folder name in square brackets, like [locale], that matches any one part of a path. So /monitoring/account matches app/[locale]/account.
- Bundler: the tool that packs your code for production. Next.js can use webpack, the older default, or Turbopack, the default for next build since Next.js 16.
What is the @sentry/nextjs vulnerability of 2026?
It is a protection failure in Sentry's Next.js SDK, the library you install to send errors to Sentry. The advisory is GHSA-mchm-gjx6-7h55, and its CVE number is CVE-2026-114139. The weakness class is CWE-693, which means a protection mechanism failed.
With tunnelRoute set, the SDK told Next.js to skip your middleware for any path under the tunnel path. But Sentry's rewrite only forwards one exact request shape. Everything else under that path fell through to your normal pages, with middleware switched off.
The advisory states the limits clearly. Skipping middleware grants no extra privileges, and routes that check the session on the server stay protected. Sentry's own services and the data in your Sentry organisation are not affected.
| Release line | Affected versions | First fixed version | Fix released |
|---|---|---|---|
| v9 | 9.36.0 up to 9.47.1 | 9.47.2 | 29 Sep 2026 |
| v10 | 10.0.0-alpha.0 up to 10.75.1 | 10.75.2 | 22 Sep 2026 |
| v11 prereleases | 11.0.0-alpha.0 up to 11.0.0-rc.0 | 11.0.0-rc.1 | 22 Sep 2026 |
| Before 9.36.0 | Outside the advisory's range; 9.35.0 has no middleware skip | Not needed | Not applicable |
The severity label depends on where you look. The GitHub Advisory Database and NVD show Critical, CVSS 4.0 score 9.1. Sentry's own advisory page shows Moderate and no CVE, as of 28 October 2026.
CVSS is the common 0 to 10 severity score. This vector includes AT:P, meaning attack requirements are present. In plain words, your app must match the four conditions before anyone can use the bug. Judge your risk on those conditions, not on the headline number.
How did the tunnel path make Next.js skip your middleware?
Two pieces of Sentry code described the tunnel differently. The rewrite was narrow, and the middleware skip was wide. Requests that fit the wide rule but not the narrow one went to your pages without any middleware check.
The narrow rule: Sentry's rewrite
The rewrite lives in config/withSentryConfig/tunnel.js in the npm package. Its source pattern is the tunnel path with an optional trailing slash. It also needs query parameters o and p, short for organisation ID and project ID, holding digits.
So only a request like /monitoring?o=123&p=456 gets forwarded to Sentry. A request for /monitoring/account does not match, so Next.js routes it like any other address.
The wide rule: the middleware skip
On webpack builds, the SDK wraps your middleware file automatically. Before your code runs, the wrapper checks whether the address is a tunnel request. If yes, it returns an empty response with the header x-middleware-next set to 1.
That header is what Next.js's own NextResponse.next() sends. It means "carry on routing". Your middleware function is never called, so its cookie check never happens.
In 10.75.1 the test is pathname equals the tunnel path or starts with the tunnel path plus a slash. Versions 9.36.0 to 9.47.1 and 10.0.0 to 10.50.0 were wider still. They used a plain startsWith check, so even /monitoringx/account was skipped.
One request on an affected webpack build
Drag to exploreNotice the gap: the skip box says yes to /monitoring/account, but the rewrite box says no. The request lands on the page with no login check.
Diagram connections:
- Visitor with no cookie to Sentry wrapper: tunnel?
- Sentry wrapper: tunnel? to Your middleware (No)
- Sentry wrapper: tunnel? to Sentry rewrite matches? (Yes: middleware skipped)
- Sentry rewrite matches? to Sentry ingest (Yes)
- Sentry rewrite matches? to app/[locale]/account (No: normal routing)
- Visitor with no cookie: GET /monitoring/account
- Sentry wrapper: tunnel?: 10.75.1: path is /monitoring or under it
- Your middleware: Would return 401 without a cookie
- Sentry rewrite matches?: Needs exactly /monitoring plus o and p
- Sentry ingest: Receives real error reports
- app/[locale]/account: Renders with locale = monitoring
Think of a housing society gate. The courier desk accepts parcels only for the office in Block A, room 1. The gate guard has a note saying anyone heading to Block A may skip the ID check.
The courier desk is the rewrite, and the guard's note is the middleware skip. Room 1 is the exact tunnel request, and the rest of Block A is every page under the tunnel path. Visitors for other rooms walk past the guard unchecked.
Where the picture breaks: a real guard might notice someone odd, but code checks only the rule it was given. And here the gap needed one more piece, a page that answers under that path.
Why does an app/[locale] folder make almost any tunnel path collide?
A dynamic segment at the root matches any first path segment, including your tunnel path. So /monitoring/account, /errors/account and /anything/account all reach app/[locale]/account.
The advisory names this case directly. A direct clash, like a real /monitoring/foo page, is easy to spot in code review. A root [locale] or [slug] folder is not, because no file mentions the word monitoring.
We checked whether renaming the tunnel helps. With 10.75.1 on a webpack build, we changed tunnelRoute to /sentry-tunnel-x9. A request for /sentry-tunnel-x9/account still returned 200 and the private page.
Setting tunnelRoute to true does not help either. The SDK then picks a random eight-character path for each build. That path has to reach the browser, and we found it in the public JavaScript files under .next/static. Our random path, /9dtlft0p, also served the private page.
Why are Turbopack builds not affected by default?
Because the SDK only wraps your middleware automatically through a webpack loader, a plug-in step that rewrites files during a webpack build. The Turbopack setup in the same package has no middleware wrapping at all.
We confirmed this in the build output. In the Turbopack build, the middleware chunk held our plain cookie-checking function. All three test addresses returned 401 on 10.75.1 and on 10.75.2.
There is one exception the advisory does not mention. The SDK also exports wrapMiddlewareWithSentry, so you can wrap middleware by hand. We did that on a Turbopack build with 10.75.1, and /monitoring/account returned 200 and the private page.
With 10.75.2, the same hand-wrapped Turbopack build returned 401. So if your middleware file calls wrapMiddlewareWithSentry, treat your app as affected on any bundler until you upgrade.
Sentry's build options page points the same way. It says that on Turbopack, your middleware can block the tunnel, and you should exclude the tunnel path with a negative matcher. It adds that without Turbopack, Sentry skips the tunnel route automatically. That automatic skip is exactly the code that was too wide.
Check which bundler built your app in the first line of next build output. On Next.js 16.4.3 it prints "Next.js 16.4.3 (Turbopack)" by default, or "(webpack)" when you pass --webpack. On older Next.js versions, assume webpack unless the log says Turbopack.
What did the fix in @sentry/nextjs 10.75.2 change?
The fix makes the middleware skip match only what the rewrite really forwards. The new function, isSentryTunnelRequest in common/utils/tunnelPathnameMatch.js, checks three things before skipping your middleware.
- The method is POST. The SDK's transport only ever sends POST requests.
- The path is exactly the tunnel path, with or without one trailing slash. Nothing under it counts.
- Every value of the o and p query parameters is present and made only of digits.
The third rule handles repeated parameters. A comment in the source says Next.js checks only the last value of a repeated parameter. So the fix demands that every value qualifies, not just one.
The same function shipped in 9.47.2 and 11.0.0-rc.1. The pull requests are 24499 for the main branch, 24565 for v10 and 24760 for v9. On v9 and v10, no configuration change is needed after you upgrade.
| Request | 9.36.0 to 9.47.1 and 10.0.0 to 10.50.0 | 10.51.0 to 10.75.1 | 10.75.2 and later |
|---|---|---|---|
| POST /monitoring?o=123&p=456 (a real error report) | Skipped | Skipped | Skipped |
| GET /monitoring?o=123&p=456 | Skipped | Skipped | Middleware runs |
| GET /monitoring/account | Skipped | Skipped | Middleware runs |
| POST /monitoring/account?o=1&p=2 | Skipped | Skipped | Middleware runs |
| GET /monitoringx/account | Skipped | Middleware runs | Middleware runs |
| POST /monitoring?o=1&o=abc&p=2 | Skipped | Skipped | Middleware runs |
The fixed version still lets real error reports through. We pointed the tunnel at a local stand-in server instead of Sentry. A POST to /monitoring?o=1&p=2 with no cookie reached the stand-in, while GET requests and /monitoring/account got 401.
The skip decision in 10.75.2
Drag to exploreEach check must pass before the SDK skips your middleware. Any No sends the request through your own login check first.
Diagram connections:
- Incoming request to Is it a POST?
- Is it a POST? to Exactly the tunnel path? (Yes)
- Is it a POST? to Your middleware runs (No)
- Exactly the tunnel path? to All o and p values digits? (Yes)
- Exactly the tunnel path? to Your middleware runs (No)
- All o and p values digits? to Forward to Sentry (Yes)
- All o and p values digits? to Your middleware runs (No)
- Incoming request: Any path, any method
- Is it a POST?: The SDK only sends POST
- Exactly the tunnel path?: /monitoring or /monitoring/ only
- All o and p values digits?: Repeated values must all qualify
- Forward to Sentry: Middleware skipped, rewrite serves it
- Your middleware runs: Cookie check returns 401 if missing
How do you check if your Next.js app is affected?
Work through five checks in order. If any one rules you out, this bug does not reach your app, though upgrading is still the simple choice.
Find your SDK version
Run npm ls @sentry/nextjs in the project folder. Compare the version with the table above. Lock files can pin an old version even when package.json allows a newer one.
Look for tunnelRoute
Open next.config.js or next.config.mjs and search for tunnelRoute inside withSentryConfig. A string path or the value true both count.
Check the bundler
Read the first line of your production build log. It names Turbopack or webpack in brackets. Webpack builds are affected, and so is any build whose middleware calls wrapMiddlewareWithSentry.
Look for routes under the tunnel path
List folders under app and pages. A folder with the tunnel name, or a root folder in square brackets, means a route resolves under the tunnel path.
Find where login is enforced
If pages, route handlers and data functions check the session themselves, the skip grants nothing. If only middleware checks it, you are exposed.
Then test it on a staging copy of your own app. Request a private page under the tunnel path with no cookie, for example curl -s -o /dev/null -w '%{http_code}' https://staging.example.com/monitoring/account.
A safe build prints 401, 403 or a redirect code such as 307 from your middleware. An affected build prints 200 and returns the private page. Only run this against apps you own.
| Build | GET /en/account | GET /monitoring/account | GET /monitoringx/account |
|---|---|---|---|
| 10.75.1, webpack | 401 | 200, private page shown | 401 |
| 10.75.1, Turbopack | 401 | 401 | 401 |
| 10.75.2, webpack | 401 | 401 | 401 |
| 10.75.2, Turbopack | 401 | 401 | 401 |
What should you do now about the Sentry Next.js tunnel bug?
Upgrade first, then fix the habit that made the bug dangerous. Both steps are small.
Upgrade the SDK
Install 9.47.2 if you are on v9 or 10.75.2 or later on v10. Moving to 11.x also fixes it, but read the next section first. Rebuild and redeploy, because the wrapper is added at build time.
Re-run the curl check
Repeat the request for a private page under the tunnel path. Expect 401, 403 or your login redirect, not 200.
If you cannot upgrade today, pick a workaround
Remove tunnelRoute, or switch the build to Turbopack without a hand wrap and add the exact tunnel check shown in the next section. Renaming the tunnel path helps only if no route resolves under it.
Add a session check next to the data
Put a session check in each private page, route handler and server action, ideally through one shared function. This protects you from the next middleware bypass too.
What changes for the tunnel when you move to @sentry/nextjs 11?
Version 11.0.0 removed the middleware skip entirely. Pull request 24589 calls keeping the old skip in sync with the rewrite fragile. It also notes that Turbopack never had it, so the two bundlers behaved differently.
Tunnel requests now pass through your middleware on both bundlers. So if your middleware blocks every visitor without a session, it now blocks Sentry error reports too. On 11.9.0 with our original middleware, a real tunnel POST got 401.
The pull request tells you to exclude the tunnel route from your middleware matcher, using a fixed string path. It adds that tunnelRoute set to true cannot be used for this, because the path is random. Sentry's build options page suggests a negative matcher such as (?!error-monitoring).
In our test app, it left the same gap, now in your own config rather than in Sentry's code. With the matcher skipping paths that start with monitoring, /monitoring/account and /monitoringx/account both returned 200 and the private page.
This is not specific to version 11. The same matcher on 10.75.2 with a Turbopack build gave the same result. It only matters when a page resolves under the excluded prefix and middleware is its only login check.
Anchoring the pattern to the exact path is not enough either. With (?!monitoring$), GET /monitoring still skipped the middleware and returned app/[locale]/page.tsx, our private locale home page. Test any matcher change on your own staging app with the curl check above.
A safer pattern keeps the matcher broad and lets through only the exact tunnel request inside the middleware. It copies the three checks of the 10.75.2 fix. This is the file we ran on 11.9.0, with both a webpack and a Turbopack build:
import { NextResponse, type NextRequest } from "next/server";
const TUNNEL = "/monitoring";
// Let through only what Sentry's tunnel rewrite forwards:
// POST to the exact tunnel path, with numeric o and p query values.
function isTunnelRequest(req: NextRequest) {
const { pathname, searchParams } = req.nextUrl;
if (req.method !== "POST") return false;
if (pathname !== TUNNEL && pathname !== TUNNEL + "/") return false;
return ["o", "p"].every((key) => {
const values = searchParams.getAll(key);
return values.length > 0 && values.every((v) => /^\d+$/.test(v));
});
}
export function middleware(req: NextRequest) {
if (isTunnelRequest(req)) return NextResponse.next();
if (req.cookies.get("session")?.value !== "ok") {
return new NextResponse("blocked by middleware", { status: 401 });
}
return NextResponse.next();
}
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };With this file, a POST to /monitoring?o=1&p=2 reached our local stand-in for Sentry. A POST to /monitoring with no o and p, and GET /monitoring/account, returned 401. The o and p check matters: without it, that bare POST rendered our private locale home page.
The 11.0.0 release notes also move withSentryConfig to the import path @sentry/nextjs/config.
| Middleware setup | POST /monitoring?o=1&p=2 | POST /monitoring | GET /monitoring/account |
|---|---|---|---|
| Broad matcher, no tunnel rule | 401: error reports blocked | 401 | 401 |
| Matcher skips paths starting with monitoring | Forwarded to Sentry | 200, private page shown | 200, private page shown |
| Broad matcher, POST and exact path check only | Forwarded to Sentry | 200, private page shown | 401 |
| Broad matcher, POST, exact path and o and p check | Forwarded to Sentry | 401 | 401 |
Why is middleware-only auth fragile in Next.js?
Because middleware is a filter in front of your pages, and filters can be bypassed by rules you do not control. A library, a matcher pattern or a framework bug can stop it running.
This has happened before. In March 2025, CVE-2025-29927 let attackers skip Next.js middleware itself, with a CVSS 3.1 score of 9.1. Fixes shipped in Next.js 12.3.5, 13.5.9, 14.2.25 and 15.2.3.
The Next.js authentication guide, last updated 16 September 2026, gives the same advice. It says Proxy should not be your only line of defense. It recommends doing most security checks as close as possible to your data source.
The guide also warns against relying on layouts for auth checks. Layouts do not re-render on every navigation, and they do not stop nested segments from running. So prefer the page, the route handler or a data access layer, a small module that every data read goes through.
Here is the page-level check from our test app. It is a toy: it compares a cookie to a fixed value. A real app would verify a signed session instead.
import { cookies } from "next/headers";
export const dynamic = "force-dynamic";
// Same page, but the page itself checks the session (auth next to the data).
export default async function Page() {
const session = (await cookies()).get("session")?.value;
if (session !== "ok") return <p>PAGE-CHECK-REFUSED</p>;
return <p>PRIVATE-BILLING-PAGE</p>;
}We placed this file at app/[locale]/billing/page.tsx on the affected 10.75.1 webpack build. GET /monitoring/billing skipped the middleware and still returned PAGE-CHECK-REFUSED. With the cookie set, /en/billing showed the private text.
Keep middleware for fast redirects and for static pages that cannot check per request. Just do not let it be the only thing between a visitor and private data.
How do you explain the Sentry Next.js vulnerability in a system design interview?
Use it as a story about defence in depth, meaning several independent checks, each able to stop an attack alone. Interviewers care less about the CVE number than about the design lesson.
State the setup
A Next.js app guarded private pages with middleware only. Sentry's tunnelRoute relayed error reports through the app's own server to avoid ad blockers.
Name the gap
The SDK skipped middleware for every path under the tunnel, but its rewrite forwarded only one exact request. The difference reached normal pages unguarded.
Explain the trigger
A root dynamic segment like [locale] matched the tunnel name as a locale. So /monitoring/account rendered the account page without a login check.
Give the fix
Versions 9.47.2 and 10.75.2 skip only POST requests to the exact tunnel path with numeric o and p values. Version 11 drops the skip, so the app must allow that exact request itself.
Draw the lesson
Keep allow-lists exactly as narrow as the thing they allow, and test both. Put authorisation next to the data so one skipped layer does not expose it.
Questions people ask
- Is my app affected if I use Sentry with Next.js but never set tunnelRoute?
- No. The middleware skip only switches on when tunnelRoute is set to a path or to true. Without it, the SDK has no tunnel path to compare against, so your middleware runs for every request. Upgrading is still sensible, since newer versions carry other fixes, but this advisory does not apply to you.
- Does this bug leak data from my Sentry account?
- No. The advisory says Sentry's own services and the data in your Sentry organisation are not affected. The risk is to your own app's pages. A visitor could reach a page that only your middleware was protecting. What they see depends on what that page shows without a session.
- Why does GitHub say Critical while Sentry's page says Moderate?
- The two pages were scored differently. The GitHub Advisory Database and NVD list a CVSS 4.0 score of 9.1, Critical, provided by GitHub as the CVE numbering authority. Sentry's repository advisory showed Moderate and no CVE, as of 28 October 2026. Your real risk depends on whether all four conditions hold in your app.
- Does switching to Turbopack fully fix the problem?
- Mostly. The SDK does not wrap middleware automatically in Turbopack builds, but a hand wrap with wrapMiddlewareWithSentry brings the skip back on affected versions. You then have to let error reports through your middleware yourself. Use the exact check from the version 11 section, not a prefix matcher. Page-level auth checks protect you either way.
- Is this the same as the Next.js October 2026 security update?
- No. That 14 October 2026 update fixed remote code execution and denial-of-service bugs in Next.js 16.4.1 and 15.5.28. This advisory is about Sentry's Next.js SDK, a separate npm package, and it lets requests skip middleware rather than run code. You may need both upgrades, so check your next and @sentry/nextjs versions separately.
- Has the Sentry tunnel had security problems before?
- Yes, a different one. CVE-2023-46729 was a server-side request forgery in the tunnel endpoint, which means the server could be tricked into calling other addresses. It affected @sentry/nextjs 7.26.0 up to 7.77.0 and was fixed in 7.77.0. It is unrelated to this middleware bug, apart from both involving the tunnel.
Sources and further reading
- GitHub Advisory Database: GHSA-mchm-gjx6-7h55 (CVE-2026-114139)
- Sentry repository security advisory GHSA-mchm-gjx6-7h55
- NVD record for CVE-2026-114139
- sentry-javascript release 10.75.2
- sentry-javascript release 9.47.2
- sentry-javascript release 11.0.0-rc.1
- sentry-javascript release 11.0.0
- Pull request 24589: remove middleware bypass for tunnel route requests (v11)
- Pull request 24499: align tunnel request matching with the tunnel rewrite
- Pull request 24760: v9 backport of the fix
- Sentry docs: Next.js build options (tunnelRoute)
- Next.js docs: How to implement authentication
- GitHub Advisory Database: GHSA-f82v-jwr5-mffw (CVE-2025-29927, Next.js middleware bypass)
- GitHub Advisory Database: GHSA-2rmr-xw8m-22q9 (CVE-2023-46729, Sentry tunnel SSRF)