Feature focus: authorization decorators, resource loading and ownership checks
Student skill: comparing permission checks vs. action targets, spotting inconsistent resource resolution
Authorization confusion happens when the code that checks permissions examines a different resource or identity than the code that performs the action. The check might ask "Can Alice access document X?" while the action operates on document Y.
Unlike authentication confusion (identity mix-ups), this is about access control: the identity is correct, but the permission check and the business logic disagree on which resource is being accessed.
- Authorization decorators/middleware
- Resource loading
- Relationship traversal
- Permission checking
- Database queries with filters
- Permission checks load/resolve resources independently from business logic
- Indirect references (e.g., IDs) resolved differently between check and action
- Parent-child assumptions break under adversarial input
- Checks run before/after data merging/normalization
- "Helpful" defaults or fallbacks in checks lead to fail-open
The platform is maturing! Sandy adds multi-tenancy support: restaurants can customize menus, manage items, and update settings. Mr. Krabs wants control over his restaurant's data without Sandy's help.
Squidward gets jealous of SpongeBob getting employee-of-the-month. Plankton intensifies gets signs up Chum Bucket as the next Cheeky SaaS customer and intensifies his attacks, probing for ways to sabotage the Krusty Krab from within the platform.
This introduces proper authorization: ownership checks, roles (customer, manager, admin), and resource scoping. Authentication methods from r02 (cookies, API keys) remain, but now handlers must verify not just who you are, but what you can access.
By the end of r03, these roles are enforced:
| Role | Description | Auth Methods |
|---|---|---|
| Public | No authentication required | None |
| Customer | Regular users placing orders | Cookie |
| Manager | Restaurant staff (auto-assigned by email domain) | Cookie/API Key |
| Admin | Platform admins | X-Admin-API-Key |
| Lifecycle | Method | Path | Auth | Purpose | Vulnerabilities |
|---|---|---|---|---|---|
| v101+ | GET | /account/credits | Customer | View balance | v202 |
| v202+ | POST | /account/credits | Admin | Add credits | v202 |
| v101+ | GET | /menu | Public | List available items | |
| v101+ | GET | /orders | Customer/Restaurant | List orders | v201, v204, v304 |
| v201+ | GET | /orders/{id} | Customer/Restaurant | Get single order | v305 |
| v105+ | POST | /orders/{id}/refund | Customer | Request refund | v105 |
| v201+ | PATCH | /orders/{id}/refund/status | Restaurant | Update refund status | v301 |
| v103+ | PATCH | /orders/{id}/status | Restaurant | Update order status | v305 |
| v103+ | POST | /cart/{id}/checkout | Customer | Checkout cart | v302 |
| v103+ | POST | /cart/{id}/items | Customer | Add item to cart | |
| v303+ | PATCH | /menu/{id} | Restaurant | Update menu item | v303, v405 |
| v306+ | POST | /restaurants | Public | Register restaurant + API key | v306 |
| v307+ | PATCH | /restaurants/{id} | Manager | Update restaurant profile | v307 |
The in-memory dict can't scale anymore. Sandy migrates to Postgres with SQLAlchemy, refactoring the entire data layer to use ORM models. While she's at it, she introduces multi-tenancy—each restaurant now gets its own API key.
The Vulnerability
- Sandy extracted a
has_access_to_order(order_id)helper that returnsTrueif either the current customer owns the order or the current restaurant owns the order. - Manager-only endpoints now rely on that helper even though they should only consider the restaurant branch.
- Plankton keeps his customer session cookie (from placing the order) and sends a Chum Bucket manager API key when patching refunds.
- The API-key authenticator marks the request as "manager" while
has_access_to_order()immediately approves the request because the still-present customer session owns the order.
Exploit
- Place an order at Krusty Krab as a regular customer (keep the cookie or stay logged in).
- Send
PATCH /orders/{id}/refund/statuswith your Chum BucketX-API-Key; no extra auth header is required because the cookie still rides along. - The manager endpoint sees the API key and trusts the helper, which immediately approves the request because your user session owns the order.
Impact: Cross-tenant privilege escalation; Plankton refunds his own purchases at competitors.
Severity: 🟠 High
Endpoints: PATCH /orders/{id}/refund/status
Sandy adds order and cart IDs to the session cookie, in order to simplify the ownership checks in the web app. The mobile app still uses Basic Auth relying on the legacy behavior, so she needs to support both. She also experiments with the new 'foolproof' way of charging the customers: the authorization check includes placing a hold on the customer's credit which gets automatically reversed if the request fails.
The Vulnerability
- The new
charge_customer_with_holddecorator resolves carts by readingsession.cart_idfirst, charging/authorizing that cart total, and then clearing the session entry. - The checkout handler runs afterwards and calls
resolve_trusted_cart()again; with the session entry gone, it falls back to the cart ID in the URL path (/<cart_id>/checkout). - A request can therefore pay for the cheap cart currently stored in the session while the handler fulfills the expensive cart referenced in the path.
Exploit
- Maintain two carts: keep a $7 “official” cart in the UI so
session.cart_idpoints to it, and separately build a $70 cart by calling the API with different IDs. - Without checking out the cheap cart, send
POST /cart/{expensive_cart_id}/checkoutfrom the browser session. - The hold decorator charges the $7 session cart, but the handler immediately re-resolves the path cart and ships the $70 items.
Impact: Plankton is charged $10 for the $100 cart.
Severity: 🟠 High
Endpoints: POST /cart/{id}/checkout
Restaurants want self-service menu updates. Sandy adds a convenience endpoint
PATCH /menu/{item_id}and extracts a reusable@restaurant_owns()decorator from her existing routes.
The Vulnerability
- The decorator expects
restaurant_idinrequest.view_argsto validate ownership. - The new
/menu/{item_id}route never setsrestaurant_id, so the decorator just verifies the item exists and exits without comparing ownership. - Handlers therefore trust whichever API key showed up, even when the item belongs to another restaurant.
Exploit
- Auth as manager of Chum Bucket (restaurant_id = 2).
- Call
PATCH /menu/1(a Krusty Krab item) with{ "available": false }. - The decorator has no restaurant ID to compare, so the request succeeds and disables the competitor’s menu item.
Impact: Cross-restaurant menu tampering.
Severity: 🟠 High
Endpoints: PATCH /menu/{id}
Aftermath: Sandy figures out she needs to standardize on a common way to pass the restaurant ID to the endpoints, so she adds a bind_to_restaurant() helper to auto-detect it and keep handlers tiny.
Sandy’s integration SDK, mobile app, and soon-to-exist manager UI all send restaurant identifiers differently, so she adds
bind_to_restaurant()to auto-detect them and keep handlers tiny.
The Vulnerability
GET /ordersuses?restaurant_id=in the query string; the decorator checks that value specifically:@require_restaurant_access(query.restaurant_id).- the database query is automatically bound to the restaurant ID using
bind_to_restaurant(), but this helper inspects all request containers (query, form, JSON).
Exploit
- Manager of Chum Bucket calls
GET /orderswith body{ "restaurant_id": 1 }. - Decorator assumes the user meant their own restaurant because the query parameter is absent.
- Database helper binds to
restaurant_id=1from the body and returns Krusty Krab data.
Impact: Full leakage of competitor orders.
Severity: 🟠 High
Endpoints: GET /orders
Aftermath: Sandy fixes the vulnerability and decides to reduce the risk of inconsistencies between the access checks and the data storage operations by pushing the authorization fully into the data layer.
The next big thing on the roadmap is the web-based manager dashboard, so to prepare for it, Sandy rewrites order updates to rely on stored procedures that double-check ownership.
The Vulnerability
- Authorization happens inside the
UPDATE ... WHERE restaurant_id = current_user.restaurant_idquery. - However, Sandy also adds idempotency checks based on the business logic, to guard against illegal state transitions.
- The handler still returns the order payload (loaded before validation), so attackers can read data they shouldn’t without ever triggering auth.
Exploit
- Guess order IDs sequentially.
- Call
PATCH /orders/{id}/statuswith an invalid transition (since the first status isPENDING, order can never transition back to it). - The handler refuses to update (no auth check) but returns the order details.
Impact: Cross-tenant order disclosure at scale.
Severity: 🟠 High
Endpoints: PATCH /orders/{id}/status
Aftermath: With guardrails “in place,” she turns to the next big blocker: letting restaurants self-register instead of emailing her.
To escape manual onboarding, Sandy finally ships restaurant self-registration. A restaurant submits a domain, receives a token at
admin@{domain}, and immediately gets an API key. She reuses her battle-tested email verification code to move fast. Sandy plans to automatically make all users with matching email domains – restaurant managers, so she sends a precautionary email to all current restaurant owners to check the list of the upcoming managers which she exposes in the API, ahead of that change.
The Vulnerability
- Token verification checks signature/expiry and ensures the claimed domain matches the email domain.
- It does not ensure the local part is
admin@—any mailbox on that domain works. - The lack of differentiation between user and restaurant domain verification tokens allows anyone who can receive any mailbox at the victim domain to onboard a restaurant with that domain.
Exploit
- Plankton registers user
plankton@bmail.seaand keeps the token. - He creates restaurant
{ "domain": "bmail.sea" }and confirms using the user token (despiteplankton@bmail.sea != admin@bmail.sea). - The platform assumes the domain is verified, issues an API key, and leaks every user tied to
bmail.sea.
Impact: User enumeration when any mailbox at the domain is available to the attacker.
Severity: 🟡 Medium
Endpoints: POST /restaurants
With self-registration live, Sandy finally exposes management actions in the website/mobile app by auto-assigning manager roles to users whose email domain matches the restaurant. Managers can now update their restaurant profile via
PATCH /restaurants/{id}.
The Vulnerability
- Sandy adds domain token verification to the profile update endpoint to support changing restaurant domains securely.
- The
@require_restaurant_ownerdecorator checks ownership using therestaurant_idfrom the URL path. - However, when a verified token is present, the handler loads the restaurant from
verified_token["restaurant_id"]instead of the URL. - The token's
restaurant_idis set when the token is requested, not when it's used—so an attacker can request a token for restaurant A (which they own), then use it while calling the endpoint for restaurant B.
Exploit
- Plankton calls
PATCH /restaurants/1(Krusty Krab) withdomain: "chum-bucket.sea". The decorator generates a token containingrestaurant_id: 1and emails it toadmin@chum-bucket.sea(which Plankton controls). - Plankton calls
PATCH /restaurants/2(his own Chum Bucket) with the token from step 1. - The
@require_restaurant_ownerdecorator checks: "Is Plankton owner of restaurant 2?" → Yes, he passes. - The handler extracts
restaurant_id: 1from the token and updates Krusty Krab's domain tochum-bucket.sea.
Impact: Full takeover of an existing tenant. Attacker can change any restaurant's domain to their own, gaining manager access.
Severity: 🔴 Critical
Endpoints: PATCH /restaurants/{id}
Aftermath: Sandy realizes the token's restaurant_id should never override the URL. She reverses the decorator order so ownership is checked before tokens are processed, and ensures the handler always uses the URL's restaurant_id.