September 12, 2026
ACCESS CONTROL, RATE LIMITS, AND SAFE FAILURE

Lesson 48.2 hardened what comes *in* and what must never go *out*. Now the three questions every route must answer together: is this request well-formed, may this identity do this thing, and may it happen this often? Miss any one of the three and the other two cannot save you.
Three questions, three different guards
Keep these apart when you brief an AI, because each has a different enforcer:
- Validation asks: *is this request well-formed?* Shape, size, allowed values. Enforced by input checks at the route.
- Authorization (authZ) asks: *may this identity do this action to this resource?* Enforced by server-side policy and data-layer rules, per Class 47.
- Rate limiting asks: *may this action happen this often without harm?* Enforced by counters, quotas, and budget caps.
A perfectly formed request from the wrong user must be denied. A legitimate user's ten-thousandth request this hour must be throttled. A throttled system must still answer politely. Specify all three on every route that reads private data, writes anything, spends money, or triggers work — which, in Research Desk, means nearly all of them.
Least privilege is the rule behind all three guards: give a person, service, or agent only the access it needs for its job — nothing more, for no longer than needed. A signed-in reader needs to save their own watchlist items, not to trigger the paid refresh worker or read another user's list. The narrow server-side refresh job needs to write source snapshots, not to read private watchlists. An admin needs audited access to operational tooling, not universal browser access to every record. When you write the route safety card below, check each actor against this question: "what is the smallest permission that still lets this job finish?" Anything broader is debt you will defend in Class 53.
Broken access: the server verifies, always
The most common application-security failure in the OWASP Top 10 family is broken access control: the system takes the caller's word for what they may do. A request's body, URL, hidden field, and UI state are *claims*, not authority. user_id in a posted form says "I claim to be this user." The item_id in the URL says "I claim this record." A hidden role=admin field says "I claim this power." None of them are proof.
The server — and, where the platform supports it, the data policy near the data — verifies every claim against the session identity and the stored ownership fact:
POST /api/watchlist/delete { item_id: "item_b_207" }
1. session → caller is usr_41 (Maya). Signature verified, expiry checked.
2. load record item_b_207 → owner_id is usr_88 (Daniel).
3. policy: owner-only delete → usr_41 ≠ usr_88 → DENY.
4. respond "not found or not available" + log denied attempt with request ID.
Note step 4's wording: for private records, a denial that says "that belongs to someone else" confirms the record exists; "not found or not available" reveals less. Pick the convention per route and document it. And the Neighborhood Events twin from Class 47 still applies: hiding the Edit button never secured anything. The route and the data policy are the enforcement points; the UI is decoration.
Limits, quotas, caps, idempotency: the refresh bill
Research Desk's refresh — one click, one paid provider call, possibly a model call — is where abuse becomes a bill. Four controls share the job:
- Rate limits bound frequency: e.g., 30 searches per minute per IP, 5 refreshes per hour per user per brief. Excess gets a clear "slow down, try again in N minutes" response, not a silent drop.
- Quotas bound totals: e.g., 100 briefs per user per month on the starter tier. Quotas make cost predictable and give the pricing page something honest to say.
- Budget caps bound money: e.g., provider spend halts at a daily ceiling and pages the owner. The cap is the circuit breaker when limits and quotas are misconfigured.
- Idempotency bounds accidents: retries, double-clicks, and agent retry loops must not create duplicate writes or duplicate charges. A client-supplied idempotency key (or a natural key like
user + ticker + hour) means "same request twice = one effect, same receipt."
Specify the numbers in the task card, not "add rate limiting." An AI told to "add rate limiting" invents numbers that match nothing; an AI told "5 refreshes per user per brief per hour, 429 with retry-after, log every throttle with request ID" builds the control you priced.
Fail safely: kind outside, precise inside
When something goes wrong — bad input, denied access, throttled request, dead provider, unknown fault — the system speaks two languages:
The customer sees a clear, non-sensitive message with a next step: "That ticker didn't look right — try AAPL." / "Sign in to save companies." / "Refreshes are paused for this brief; try again in 20 minutes." / "Something went wrong on our side. Reference req_9f3c if you contact support." No database errors, no stack traces, no raw provider messages, no hint of whose record that ID belongs to.
The log keeps everything the responder needs: timestamp, request ID, route and action, safe actor/resource references (user ID, record ID — never passwords, tokens, or full private content), outcome class, duration, and dependency result. The request ID is the thread that stitches the customer's report to the server's record — Class 51 will build the full observability story on this.
Alerting sits between the two: a single denied request is routine; a hundred denials from one source in a minute, or a sustained spike of 500s after a deploy, is a signal worth waking someone for. Name the signal, the threshold, and the owner now, while the route is small.
Three concepts to configure deliberately
Three web-security names deserve concept-level understanding — what they guard, not a vocabulary dump:
- CORS (cross-origin resource sharing) decides which *other websites'* browsers may call your API directly. Default posture: your frontend's origin only; every additional origin is a deliberate, documented exception. Draw it on the request-flow diagram: browser at origin A calling API at origin B — allowed or not, and why.
- CSRF (cross-site request forgery) is the attack where a malicious site tricks a signed-in user's browser into firing an authenticated request (a
POST /api/watchlist/deletehidden in a page the victim visits). Mitigations depend on your framework and session design — same-site cookie settings, anti-forgery tokens, origin checks — so specify "follow the auth provider/framework's CSRF design" rather than inventing one. - Redirects are trust decisions: after sign-in or an action, *where* may the app send the user? Accept only a bounded allowlist of internal paths; never follow a
?next=parameter to an arbitrary external URL. An open redirect turns your domain's reputation into someone else's phishing lure.
For all three, the task card should say which framework/provider mechanism is authoritative and require the request-flow diagram to show where the check sits.
Exercise: the route safety card
For one route — POST /api/watchlist is the canonical choice — write a route safety card and attach it to the route's task card:
ROUTE SAFETY CARD — POST /api/watchlist
Identity: session required; caller usr_* established server-side.
Policy: any signed-in user may create own items; owner_id := caller id.
Validation: ticker 1–5 letters (+ optional .XX); reject empty/oversized/foreign.
Rate/budget: 20 saves/hour/user; refresh path capped separately; 429 + retry-after.
Idempotency: natural key (user+ticker); duplicate returns existing item.
User-facing errors: signed-out → sign-in prompt; invalid → ticker guidance;
denied → "not available"; throttled → wait time; fault → message + request ID.
Log event: watchlist.save { outcome, requestID, userID, ticker, duration }.
Never log: tokens, session values, other users' content.
Disable switch: flag to halt saves without deploy; owner + runbook link.
Then prove it with tests: owner allowed, other user denied, anonymous denied, oversized ticker rejected, sixth refresh throttled, double-submit creates one record, fault returns a clean message with a request ID that appears in the log.
Finish line: a route safety card attached to the task card, with identity, policy, validation, rate/budget, idempotency, user errors, log event, and disable switch — plus tests for each line.
Verify: replay the three abuse stories from Lesson 48.1 against this route and confirm each denial, each message, and each log entry.
Common failure: specifying validation without authorization ("the form checks, so we're fine") or limits without numbers ("handle abuse somehow"). All three guards, concretely, or the card is a wish.
Check your understanding
1. A request arrives with owner_id in its body matching the caller's claim. Why must the server still verify against stored data? 2. What does each of rate limit, quota, budget cap, and idempotency bound? 3. What goes to the customer versus the log when a provider call fails?
Next, Lesson 48.4 looks *outward* — dependencies, configuration, and how to get a genuinely useful AI security review without mistaking it for a certificate.
ARTICLE DISCUSSION
JOIN THE
CONVERSATION.
Got a question, a take, or a better way to do this? Log in and leave a comment.
