Testing Security Rules
Learning Objectives
- You can turn one application permission or request-protection rule into a permitted/denied comparison.
- You can make a negative test reach the intended guard and inspect the state it protects.
- You can reuse existing tests at their appropriate boundary.
We have followed a note edit through identity, request protection and ownership checks. The owner should be able to save a valid change, while another signed-in user should be denied. How can we tell that the server applies those rules when a caller sends a request directly?
A security test checks both the response and the protected state: an allowed operation takes effect; a denied one leaves that state unchanged. Let’s turn that expectation into a comparison we can observe.
Choose a permitted/denied pair
We will transfer the note-editing rule to a document. The document exercise in Chapter 5.4 used author and reviewer relationships; here, only the author may edit the title. We also include a version in this chapter’s test example, so we can inspect both fields after rejection.
Start with the CSRF rule from Chapter 5.6. Use the author’s current session and correct origin for both requests. First send an allowed edit with the matching token. Then send a different valid title with an incorrect token. Predict both the response and the stored title before comparing the two cases:
| Case | What reaches the handler | Required observation |
|---|---|---|
| Permitted edit | Current session, correct origin/token and valid title | Successful response and the new stored title |
| Denied edit | Same session and origin; incorrect token and a different valid title | 403 and the stored title remains as it was immediately before this request |
The successful request is the control: it detects a handler that always returns 403. We record the stored document after that request. The denied request proposes a different valid title so an accidental write would be visible.
Make the test reach the intended rule
Now consider the outsider’s edit. To test ownership, we use the outsider’s own current session and matching token, a correct origin, and a valid body. These conditions let the request reach the ownership check.
Figure 1 locates the two possible 403 responses: the request guard can stop a request before ownership is checked.
A 403 from the wrong boundary
0 / 5 points
An outsider edit omits X-CSRF-Token, receives 403, and is reported as proof of ownership enforcement. What is wrong with the conclusion?
A 403 is only part of what we need to observe. A broken handler could write the outsider’s title before returning 403. Check the stored title and version as well as the status.
Let’s isolate that ownership rule in a small test. The example below keeps the
document in memory. The test supplies
verifiedActor as a trusted identity and assumes session and CSRF checks have
succeeded. The request cannot choose that identity, and both bodies contain
valid titles. This isolates the ownership rule.
import assert from "node:assert/strict";
Deno.test("only the author can change the document", async () => { const document = { authorId: 7, title: "Draft", version: 1 }; let verifiedActor = { id: 7 };
const handle = async (request: Request): Promise<Response> => { const actor = verifiedActor; const { title } = await request.json(); if (actor.id !== document.authorId) { return new Response(null, { status: 403 }); } document.title = title; document.version += 1; return Response.json({ document: { ...document } }); };
const edit = (title: string) => handle( new Request("http://example.test/document", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title }), }), );
const permitted = await edit("Reviewed draft"); assert.equal(permitted.status, 200); assert.equal(document.title, "Reviewed draft"); assert.equal(document.version, 2);
verifiedActor = { id: 8 }; const beforeDenied = { ...document }; const denied = await edit("Outsider's replacement"); assert.equal(denied.status, 403); assert.deepEqual(document, beforeDenied);});After the permitted edit, the title should be Reviewed draft and the version
should be 2. The outsider then proposes Outsider's replacement. The final
assertion compares the whole document with a snapshot taken immediately
before denial. For a database-backed operation, read and compare the stored fields
instead. Cookie resolution and CSRF enforcement need separate checks.
Denial is more than a status
0 / 5 points
A broken handler writes the outsider’s title and then returns 403. What observation detects this despite the expected status?
Reuse each check for its purpose
| Check | What it contributes |
|---|---|
| Authentication/session check | Credential verification, expiry and revocation behavior |
| Pure policy and request-boundary checks | Allowed and denied relationships, plus the state changed by the operation |
| Database/concurrency check | Protected writes and invariants when operations overlap |
| Browser cookie workflow | Actual cookie transport through the configured client and server |
| Typed-client source check | Agreement between the API and client at build time |
As we connect these pieces, we can reuse the checks for the behavior each one observes. After an integration change, rerun existing checks for the affected boundary. A policy test does not observe browser cookie behavior; a browser journey may miss forbidden raw requests.
A passing suite covers the behaviors and environment it exercised. The OWASP ASVS project offers further reading on application security verification.
Next, we will apply these ideas to the marketplace. The same owner/outsider comparison will help us check that a rejected draft edit leaves its stored title and version unchanged.
Check Your Understanding
- What would a permitted control detect that denied requests alone would miss?
- Which request conditions must be valid to isolate an ownership check?
- Why is the stored value needed in addition to a 403 assertion?
- Which kind of check observes real browser cookie behavior, and why can a pure policy test not replace it?