# Release a tenant-scoped receipt import

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.

You own a receipt-import API. Review the proposed change, specify the repair, and write the evidence a reviewer would need before release.

## Supplied material

```text
Actors: Ana = user u1, tenant north; Ben = user u2, tenant north; Cy = user u3, tenant south.
Invoice I-7: tenant north, owner u1, amount 2400 cents, status pending.
Invoice I-9: tenant south, owner u3, amount 1700 cents, status pending.
Policy: only the owner in the active tenant can attach a receipt. Uploads remain private while quarantined.
POST /invoices/{id}/receipts accepts invoiceId, tenantId, filename and bytes.
Proposed handler: SELECT * FROM invoices WHERE id = '<route id>'; check row.tenant == body.tenantId; write bytes to /public/<filename>; return its URL.
Payment provider contract: verify its signature over the exact raw body, then parse the event; delivery is at least once. Event identity is (provider account,event ID).
Deliveries: D1 valid signature, account merchant-A, event e44, invoice I-7, amount 2400; D2 is a byte-identical retry; D3 same event ID but invalid signature; D4 valid event e45 for I-7 but amount 2300.
Local accepted event and payment must be committed in one transaction. No external money transfer is part of this fixture.
An upload-service token was copied into a CI log; deleting that log does not revoke it.
```

## Task 1

Mark each source of identity and data as authoritative or caller-controlled. List the independent query, permission, file-publication and credential problems.

Your assessment:

## Task 2

Write a replacement handler contract with parameter binding, owner-and-tenant enforcement, generated storage IDs, a private quarantine state and authorized download.

Your assessment:

## Task 3

Build a decision table for Ana/Ben/Cy attaching to I-7 and Ana attaching to I-9. Add ordinary and SQL-metacharacter route inputs.

Your assessment:

## Task 4

Specify D1–D4 payment outcomes and an atomic event/effect transaction. Record token revocation, replacement and affected-consumer checks.

Your assessment:

## Decide the fixed cases

### What happens to each payment delivery?

Choose from: Record the event and mark I-7 paid; Return the saved outcome; no second effect; Reject before trusting the event; Authenticated, but no payment state change.

- D1: valid signature, merchant-A, event e44, I-7, 2400: ______
- D2: a byte-identical retry of D1: ______
- D3: same event ID, invalid signature: ______
- D4: valid event e45 for I-7, amount 2300: ______

### Who may attach a receipt?

Choose from: Allowed; Denied.

- Ana (u1, north) attaches to I-7: ______
- Ben (u2, north) attaches to I-7: ______
- Cy (u3, south) attaches to I-7: ______
- Ana (u1, north) attaches to I-9: ______

## Evidence

- Four-row actor/object matrix with reasons.
- Upload lifecycle diagram and safe query shape.
- Delivery/effect ledger including invalid and mismatched events.
- Credential containment and closure checklist tied to the leaked token.

## 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. Separates query safety, actor/tenant/owner policy and file state. (2 points)

- 0 points: Names one defect, usually the SQL string, and treats it as the whole fix, or says parameter binding also enforces ownership. File publication is not mentioned.
- 1 point: Lists the query, permission and file problems but merges two of them: binding the route ID is offered as the tenant fix, or the file name is cleaned without a private quarantine state.
- 2 points: Lists each defect with its own control: a bound parameter for the route ID, tenant and owner taken from the authenticated session rather than the body, a generated storage ID in a private quarantine state, and an authorized download.

Your score:

### 2. Preserves the legitimate owner path and rejects every prohibited actor/object pair. (2 points)

- 0 points: The decision table is missing, allows Ben or Cy, or denies Ana→I-7 as well (a deny-all fix).
- 1 point: Ana→I-7 is allowed and at least two prohibited pairs are denied, but a reason is missing or wrong, or no SQL-metacharacter route input is included.
- 2 points: Four rows with their decisive reason: Ana→I-7 allowed; Ben denied (not the owner); Cy denied (other tenant, not the owner); Ana→I-9 denied (other tenant, not the owner). An ordinary and a metacharacter route ID, such as I-7' OR '1'='1, are both treated as data.

Your score:

### 3. Authenticates before deduplication and makes event acceptance plus the local effect atomic. (2 points)

- 0 points: Parses or deduplicates before verifying the signature, lets D3 claim event e44, or counts D2 as a second payment.
- 1 point: D1 to D3 are right, but D4 is paid as 2300, or the event record and the payment update are separate writes.
- 2 points: The ledger shows D1 accepted once and I-7 paid 2400; D2 returning the stored outcome with no new effect; D3 rejected before its event ID is trusted; D4 authenticated but refused for the amount mismatch. The event insert and the payment update commit in one transaction, with (provider account, event ID) unique.

Your score:

### 4. Specifies revocation and post-rotation evidence instead of treating log deletion as closure. (2 points)

- 0 points: Closes the incident by deleting the CI log, or by changing the value in CI settings only.
- 1 point: Revokes and replaces the token, but gives no evidence that the old token is now refused or that its consumers still work.
- 2 points: The checklist revokes the copied token at its issuer, issues a replacement, updates every consumer, shows a request with the old token failing and each consumer succeeding with the new one, and keeps the log-access review as a separate item.

Your score:

Critical failures (resolve before calling the case complete):

- [ ] Allows a client-supplied tenant to grant access.
- [ ] Counts a repeated delivery as another payment.
- [ ] Publishes quarantine files.

## Implementation option: Secure a real HTTP API

Repair invoice authorization and the profile update in exercise.mjs until all 14 tests pass, then add a test of your own for the metacharacter row of your decision table: a request for an invoice ID such as one' OR '1'='1 must return 404 like any other unknown ID.

Keep: The failing starter output, the passing output after your repair, and the test you added with the row of the decision table it covers.

Download: https://softwaresecurity.com/downloads/projects/express-api-519f6cf7960d8d96.zip (Node.js 22+ · npm, 14 tests). Instructions: https://softwaresecurity.com/labs/#project-express-api

## Worked assessment (read after completing the case)

D1 records one accepted event and changes I-7 from pending to paid for 2400. D2 returns the saved outcome without another effect. D3 is rejected before trusting the event identity. D4 is authenticated but inconsistent with the invoice amount and must not change payment state. Only Ana→I-7 may attach; Ben fails ownership, Cy fails tenant and ownership, Ana→I-9 fails tenant and ownership. Tenant identity comes from authenticated context, not body. Query parameterization and object permission solve separate defects. Quarantine bytes are not public. Close the credential incident only after the old credential is unusable and legitimate consumers work with its replacement.

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