# Protect a limited-seat checkout behind a browser

Use the complete synthetic fixture below in a notebook or text file. No live target, paid service or secret is needed. Make your own decision before opening the worked assessment. Then take the implementation option below, a local project in which you repair running code until its tests pass, and keep its test output with your written answers.

A browser-only ticket checkout combines cookies, a rich-text receipt and concurrent stock updates. Produce a control and test plan that preserves ordinary purchases.

## Supplied material

```text
Origin: https://tickets.example.test. Session cookie is Secure, HttpOnly, SameSite=Lax. POST /buy requires an Origin exactly equal to that origin and a CSRF token bound to the active session; missing/opaque Origin is denied for this browser-only endpoint.
Stock: one seat; Alice and Bob each have enough credit. Both requests read stock=1 before either writes. A purchase must decrement stock and charge credit in one transaction.
Rich-text receipt accepts links and basic emphasis only. Current renderer assigns user text to innerHTML.
PATCH /profile copies every supplied JSON property into the user record, including role and tenantId.
ZIP attachment entries: receipt.txt, ../server/config, receipts/ok.txt. Ordinary nested entries are allowed; traversal is not.
Test requests: A valid Alice token+origin; B valid Alice cookie with attacker.test origin; C Alice cookie with Bob’s CSRF token; D missing Origin; E Alice request with role=admin; F two simultaneous valid buys.
```

## Task 1

Make an A–F expected-outcome table. State what SameSite, Origin, CSRF tokens and authorization each establish.

Your assessment:

## Task 2

Describe the rich-text pipeline and separate safe plain-text rendering from sanitized rich-text parsing.

Your assessment:

## Task 3

Write the checkout invariant and a concurrent test with observable final stock, charge and receipt counts.

Your assessment:

## Task 4

Specify profile field allowlisting and archive containment checks, then choose at least one ordinary-use case for every denial rule.

Your assessment:

## Decide the fixed cases

### What is the expected outcome of each test request?

Choose from: Allowed; Denied before any business effect; Role and tenantId stay unchanged; At most one purchase commits.

- A: valid Alice token and Origin: ______
- B: Alice's cookie with the attacker.test Origin: ______
- C: Alice's cookie with Bob's CSRF token: ______
- D: missing Origin: ______
- E: Alice's request with role=admin: ______
- F: two simultaneous valid buys of the last seat: ______

### After request F

- Seats left in stock: ______
- Charges made: ______

### Keep or reject each ZIP entry?

Choose from: Keep; Reject.

- receipt.txt: ______
- ../server/config: ______
- receipts/ok.txt: ______

## Evidence

- Six-case browser/API decision table.
- Rendering and property-update design.
- Concurrent checkout assertion set.
- Archive allowed/rejected entry table.

## Rubric

Score each criterion with the anchor whose evidence your answer shows: 0, 1 or 2 points. Every anchor describes something a reviewer can point to in your work. Aim for at least 7 of 8, with no unresolved critical failure.

### 1. A–F decisions preserve ordinary use and distinguish each browser control. (2 points)

- 0 points: Relies on SameSite or CORS alone, or the table denies A.
- 1 point: A allowed and B to D denied, but the reasons blur (“blocked by SameSite” for C), or E and F are not decided.
- 2 points: A allowed; B denied by the exact Origin check; C denied because the CSRF token is bound to Bob’s session; D denied because a missing Origin is refused for this browser-only endpoint; E keeps role and tenantId unchanged; F commits at most one purchase. It also states what each control establishes: SameSite limits when the cookie is sent, Origin names the requesting site, the token ties the request to this session, and authorization decides the action.

Your score:

### 2. HTML and profile inputs cannot acquire script or privileged-field authority. (2 points)

- 0 points: Keeps innerHTML with hand-written escaping or a regex filter, or copies every JSON property.
- 1 point: Uses a plain-text sink or a sanitizer, but with no link policy, or allowlists profile fields without a test that role and tenantId stay unchanged.
- 2 points: Plain receipts render with textContent; rich text goes through a maintained sanitizer that allows links and emphasis only, with http and https links; PATCH /profile copies an explicit list of fields; tests show an injected image with an onerror handler stays inert and a role=admin request leaves role and tenantId unchanged.

Your score:

### 3. Concurrent assertions check committed business effects, not just response codes. (2 points)

- 0 points: Asserts two HTTP 200 responses, or one 409, without checking stored state.
- 1 point: Checks the final stock but not charges or receipts, or sends the two requests one after the other.
- 2 points: Two genuinely concurrent purchases, both reading stock 1 first, then assertions on stored state: stock 0, one purchase, one charge, one receipt, and a clear failure with no charge for the other request, enforced by a transaction or a conditional decrement.

Your score:

### 4. Archive policy rejects traversal while preserving nested ordinary files. (2 points)

- 0 points: Rejects every entry that contains a slash, or extracts ../server/config.
- 1 point: Rejects ../server/config but tests only that entry, with no ordinary nested file.
- 2 points: receipt.txt and receipts/ok.txt are extracted inside the target; ../server/config is rejected by resolving the destination and checking it stays under the target directory; every denial rule in the case (CSRF, rendering, fields, archive) has at least one ordinary-use test that still passes.

Your score:

Critical failures (resolve before calling the case complete):

- [ ] Treats SameSite or CORS as sufficient CSRF enforcement.
- [ ] Both concurrent requests charge for the single seat.
- [ ] Allows role or tenantId mass assignment.

## Implementation option: Make a checkout transaction atomic

Reproduce the race with reproduce_race.py, then repair exercise.py until all 3 tests pass: two purchases of 80 must give one paid, one declined and a balance of 20.

Keep: The reproduction output (final balance -60), failing and passing test output, and a note of the statement that now enforces the invariant.

Download: https://softwaresecurity.com/downloads/projects/checkout-sqlite-0f06facdf0c0bb1b.zip (Python 3.11+ · standard library, 3 tests). Instructions: https://softwaresecurity.com/labs/#project-checkout-sqlite

## Worked assessment (read after completing the case)

A may purchase; B, C and D are denied before business effects by this endpoint contract. E must not modify role or tenantId; reject unknown protected fields or ignore them under an explicit documented API contract and verify they remain unchanged. F must yield at most one committed purchase, one charge and stock 0; a transaction/conditional write must preserve that invariant under concurrency. A plain-text sink avoids HTML interpretation; rich-text use requires a maintained sanitizer with a narrow policy and safe links. The two normal archive paths may be retained; ../server/config is rejected. These are independent controls: CORS or CSP does not replace CSRF, field authorization or atomic checkout.

Original teaching material: CC BY 4.0. Attribution: Software Security.
