Server Actions, route handlers and proxy as public endpoints
Each entry point is a public door; it authenticates, authorizes the object and validates the input itself.
Predict first
Before you read: predict
Viewing a score on ExamHall's dashboard runs through the route handler GET /api/attempts/:id. ExamHall's proxy.ts has matcher: '/dashboard/:path*'. Mia, a candidate signed in but not the owner, sends a direct request to that handler for Ben's attempt att-ben. This lesson's fixture switches where the ownership check sits per request (the signed-in user is a test header standing in for the session): once only in proxy.ts, once inside the handler.
What does the handler return in each placement?
The 30-second version
Every Server Action and route handler is a public endpoint, reached by a direct request, not its button. A page check renders the UI; it never runs for a direct request. proxy.ts is coarse gating whose matcher can miss a path. Each entry point checks the session, the object and the input.
Each entry point is a public door; it authenticates, authorizes the object and validates the input itself.
How it works
Step 1 of 12
In the kitchen
- the stage door, the scene dock and the pass door, each reaching backstage → each route handler and Server Action, a public entry point (steps 1–2)
Analogy: A playhouse's backstage doors (theatre)
A playhouse has a stage door, a scene-dock delivery door and a pass door from the foyer; each reaches backstage, and each has its own keeper checking a pass for the area. The commissionaire out front only directs people to a door.
| At the playhouse | In the Next app |
|---|---|
| the stage door, the scene dock and the pass door, each reaching backstage | each route handler and Server Action, a public entry point (steps 1–2) |
| a keeper at each door checking the caller's own pass for that area | authentication and per-resource authorization inside each handler (steps 5–6) |
| the box office saying who may see the show, which opens no backstage door | a page-level session check that renders UI, not run for a direct request (steps 3–4) |
| the commissionaire on the street directing patrons | proxy.ts: coarse routing and optimistic gating (step 8) |
| a side alley the commissionaire never stands in | a path the matcher does not cover (step 9) |
In real terms: a request can arrive at any door directly, so the lock is on the door, not on the signpost in the street.
Breaks down when: a keeper recognises a regular face. A handler knows only the session and the request, so it re-checks every time.
Examples, run for real
4.1 ExamHall EXH-2440: a direct request past the page
ExamHall's dashboard checked the session before rendering the button, but GET /api/attempts/:id read the attempt with no check of its own. A direct request from a signed-in candidate reached it. MR !2740 moves the authentication and the owner check into the handler:
4.1 before MR !2740 (check only in the page): Mia's direct GET /api/attempts/att-ben -> 200, score 18 (the handler's JSON came back; no page HTML was rendered for it)
4.1 after MR !2740 (check in the handler): Mia -> 404 · Ben (the owner) -> 200, score 18
The page check never ran for the direct request (step 3). After the MR, the handler itself refuses a non-owner, the same 404 as a missing id (steps 5–6; sec-bola-idor).
4.2 ExamHall: the proxy that missed a path
ExamHall put its only authorization in proxy.ts, whose matcher guarded /dashboard. A request to /api/attempts/:id never passed through the proxy:
4.2 proxy matcher (from the built config): /dashboard/:path* · covers /dashboard: true · covers /api/attempts/att-ben: false
4.2 the proxy ran on /dashboard (x-proxy-ran 1) but not on /api/attempts/att-ben (x-proxy-ran absent)
The matcher, read from the built app's own config, covers /dashboard but not /api (step 9), so the proxy did not run on the API path. Authorization in the proxy alone leaves that door open.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 7
On this lesson's fixture (one handler switched per request, the signed-in user a test header), a ledger team's MR edits one billing view in three places, each tested as the MR leaves it.
Which one should the review block?
Question 2 of 7
ExamHall's proxy.ts has matcher: '/dashboard/:path*'. The compiled matcher is read from the built app's config manifest and tested against four paths: /dashboard, /dashboards, /dashboard/attempts/att-ben, /api/dashboard/export.
Which paths does the proxy run on?
Question 3 of 7
In a design round you move ExamHall's authorization into each route handler. The interviewer follows up: "We already check the session in proxy.ts for every matched route. Why not keep the authorization there and leave the handlers thin?" You give one answer.
Which answer survives the interviewer's next question?
Question 4 of 7
The interviewer asks: "ExamHall's submitAnswer is a Server Action, only ever called by the exam form's button. The button is behind a signed-in page, so the action is safe without its own check, right?" You give one answer.
Which answer survives the interviewer's next question?
Question 5 of 7
The interviewer says ExamHall has Server Actions, route handlers and a proxy.ts. Write your answer in at most 150 words, as a short list.
How do you make sure none of these entry points can be misused, and how would you test it?
Question 6 of 7
ExamHall's dashboard pages share one layout.js. This lesson's fixture switches where the ownership check sits per request (the signed-in user is a test header standing in for the session): in placement (1) only in that shared layout, in placement (2) in a helper that the route handler GET /api/attempts/:id calls. Ben, signed in, sends a direct request to the handler for Mia's attempt att-mia.
What does the handler return in each placement?
Question 7 of 7
ExamHall adds a new route handler at GET /api/exports/:id. Its proxy.ts matcher is ['/dashboard/:path*', '/account/:path*']. The compiled matchers are read from the built app's config manifest and tested against /dashboard/x, /account/y, /api/exports/e1.
Which does the proxy run on?
Go deeper
Pro tip
- In a review of a Next app, list every route handler and Server Action and ask "what does a direct request as a non-owner get?" When: an MR adds or changes an entry point (
mr-security-lens,mr-entry-point-tracing). Status: SELF (this lesson's practice target; not run).
Source
Valid as of 2026-10-01 for the advisory; the Next guides as saved. The behaviour this lesson executes (a direct request reaching the handler; the matcher coverage) is run in the fixture app, not taken from these pages.
- Next.js, Data security in Next.js (the row's first source), https://nextjs.org/docs/app/guides/data-security (
next-data-security.txt), read 2026-10-01 (steps 1–6). As of 2026-10-01. - Next.js, Proxy (the row's second source;
proxy.ts, formerlymiddleware.ts), https://nextjs.org/docs/app/getting-started/proxy (next-proxy.txt), read 2026-10-01: "Starting with Next.js 16, Middleware is now called Proxy to better reflect its purpose." (step 8). The matcher behaviour (which paths it covers) is executed in the fixture, not quoted. As of 2026-10-01. - GitHub Security Advisory, GHSA-f82v-jwr5-mffw / CVE-2025-29927 (the row's third source), https://api.github.com/advisories/GHSA-f82v-jwr5-mffw (
ghsa-f82v-jwr5-mffw.txt), read 2026-10-01: "For Next.js 15.x, this issue is fixed in15.2.3" (steps 10–11). The advisory's bypass mechanics are neither quoted nor reproduced (CL-031); only the class and the fixed versions are cited. As of 2026-10-01. - Next.js, Route Handlers (saved with the row's sources on 2026-10-01), https://nextjs.org/docs/app/getting-started/route-handlers (
next-route-handlers.txt), read 2026-10-01 (steps 1, 3–4). As of 2026-10-01.
Related lessons
- Read first: Server Components vs Client Components
- Read first: Types vanish at runtime, validate at the boundary
- Read first: Broken object-level authorization: whose record is this?
- Related: Authorization models and enforcement
- Related: Rendering modes: static, dynamic, ISR, streaming, PPR
- Related: The threat map: OWASP Top 10:2025, API Security Top 10:2023 and baseline headers
Checked against the tools and docs as of 2026-10-19.