You added a non-document route for your own pages to call, and outside callers found it. Why was it reachable, and how do you harden it?
answer
- no interface stands in front of a URL
- obscurity is not access control
- the handler is the only code every caller runs
- authenticate, then authorise the specific record
basics
~20 sBecause an endpoint is just a URL - the one surface of the deployment an outside caller reaches directly, with no interface in front of it. Harden it in the handler: authorise the caller, validate every input, limit the rate.
solid answer
~50 sIt was reachable because a handler answers any request matching its method and path. No navigation has to happen first, and the caller need not be a browser. URLs leak through network panels, logs, shared links and the client bundle that fetches them, so "nothing links to it" and "the path is long" are obscurity, not access control. The fix belongs in the handler, the only code that runs for every caller: establish who the caller is from something the request **proves**, then check that this caller may perform this operation on this record - having a session and being permitted to act are different questions. Validate every input independently of the form that usually sends it, since a disabled control constrains nobody. Assume the caller is not your browser, so make any cross-site defence explicit. Then rate-limit and cap payload size.
go deeper
Remember that anything reachable by URL is reachable by anyone, and that a check performed in the browser is not a check at all.
Explain why the handler is the enforcement point, and separate the two questions it answers: who is this caller, and may this caller do this to this record.
Walk the hardening sequence for a live endpoint: find current callers in the logs, add checks in log-but-allow mode, flip to deny, then rate-limit and cap payloads without breaking a legitimate integration.
Set the standard: every new endpoint names its intended outside caller and what proves that caller is allowed, and an endpoint with no such caller gets retired rather than documented.
A page is protected by the fact that a person has to navigate to it and the interface only offers what the session is allowed to do. **Neither of those is enforcement**, and for an in-app HTTP endpoint neither is even present. The endpoint is the one surface of a frontend deployment an outside caller reaches **directly**, with no interface in front of it to imply who may call it or with what. ## Why it was reachable It was reachable because that is what a route is. A handler answers any request whose method and path match — there is no requirement that a page rendered first, that a navigation occurred, or that the caller is a browser at all. URLs leak by every ordinary mechanism: - the network panel and history of anyone using your app; - access logs, proxy logs and error reports; - a copied link, a shared command line, a screenshot in a ticket; - the client bundle itself, which contains the path your own code fetches; - undirected scanning of plausible paths. So "nobody links to it" and "the path is long" describe **obscurity**, which is not an access-control mechanism. It fails silently: you learn that it failed only when somebody tells you. ## What actually holds The handler is the only code that runs for *every* caller, so the decision has to live there. 1. **Authorise in the handler, per request.** Establish who the caller is from something the request **proves** — a credential the handler verifies, a signature on a callback, a token exchanged for a known identity — and then check that *this* caller may perform *this* operation on *this* record. Knowing that a session exists is not the same statement as knowing the caller may act; presence and permission are different questions, and the second is the one the handler owes. 2. **Validate every input independently of the form that usually sends it.** The payload your interface sends is one of infinitely many the handler may receive. Constrain types, ranges, lengths, enum membership and identifiers server-side; never lean on a control being disabled, a field being read-only, or a value being hidden. 3. **Do not assume a browser.** An outside caller sets any header it likes, sends any origin, and may send no cookies at all. Assumptions about referrers, custom headers and same-origin behaviour are conveniences, not guarantees, and for state-changing operations reachable from a browser the cross-site request forgery defence has to be explicit. 4. **Bound the cost.** Rate-limit per caller and per operation, cap the request body, cap the work per request, and make the failure path cheap. This is the part of the deployment an anonymous caller can hammer without ever loading a page. 5. **Say as little as possible on failure.** Uniform error shapes, no stack traces, and no distinguishing "no such record" from "not yours" unless that distinction is deliberately public. ## Where the coarse checks fit | Layer | What it can do | What it cannot do | |---|---|---| | The interface | hide and disable what the session cannot use | stop anything — it does not run for other callers | | A shared request interception step | block whole paths cheaply, before the handler runs | decide whether *this* caller may touch *this* record | | The handler | the full decision, with the target record in hand | help routes it does not run for | A coarse front gate is useful precisely because it is cheap, and dangerous precisely when a team believes it is the whole answer. The fine-grained decision needs the identity and the target object together, and only the handler has both. ## Hardening an endpoint that is already exposed 1. **Find out who calls it** — access logs by path, grouped by caller, source and user agent over a meaningful window — before changing anything, so you do not break a legitimate integration you had forgotten about. 2. **Add authorisation and validation in the handler**, in a log-but-allow mode first if the traffic is unknown, then flip to deny once the rejected set is only what you expect. 3. **Rate-limit, then cap payload sizes.** 4. **Retire rather than harden** where the endpoint exists only for your own pages and the test for exposing one never applied. Let the render, or the route's own write handling, call the module directly. 5. **Add a review habit**: every new endpoint is introduced together with the answer to "who, outside this deployment, is meant to call this, and what proves they are allowed to?" ## What separates a strong answer Weak answers reach for obscurity or for the interface. Strong ones say plainly that the handler is the security boundary, that authentication and authorisation are separate checks, that input validation is independent of the usual sender, and that the endpoint should probably not have existed unless an outside caller justified it.
- The interface already hides the action from users who cannot perform it. Why is that not enough?Because it only runs for people using your interface. A hidden or disabled control shapes the experience of a cooperative browser and constrains nothing else - a script, a native client or a curious user with the network panel open sends the request regardless. Interface state is a usability feature; the handler is the enforcement point.
- How do you find out who is already calling an endpoint before you lock it down?Read access logs filtered to that path, grouped by caller identity, source and user agent over a meaningful window. Then add the new checks in a log-but-allow mode first, so you can see which real requests would have been rejected, and flip to deny once the rejected set is only what you expect. Locking down blind breaks integrations you forgot existed.
- The endpoint exists only for your own pages. Should you harden it or delete it?Apply the test for exposing one: no outside caller and a document-shaped response means it should not have existed. Moving the rule into a server-side module that the render or the route's own write handling calls directly removes the surface entirely, which is strictly better than guarding it. Harden it only if something you cannot redeploy already depends on it.
saying these in an interview costs you the question
- Assumes an endpoint nobody links to is not discoverable.
- Relies on the interface to prevent calls the handler still permits.
- Trusts the payload because the app's own form usually sends it.
- Believes the caller is always a browser the app controls.
- Treats a hard-to-guess path as an access-control mechanism.
- Confuses having a session with being allowed to touch a record.