Authentication, Authorization & Security

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:

CaseWhat reaches the handlerRequired observation
Permitted editCurrent session, correct origin/token and valid titleSuccessful response and the new stored title
Denied editSame session and origin; incorrect token and a different valid title403 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.

Figure 1. A denied ownership test must reach the ownership check; a 403 status alone does not establish which guard ran.

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

CheckWhat it contributes
Authentication/session checkCredential verification, expiry and revocation behavior
Pure policy and request-boundary checksAllowed and denied relationships, plus the state changed by the operation
Database/concurrency checkProtected writes and invariants when operations overlap
Browser cookie workflowActual cookie transport through the configured client and server
Typed-client source checkAgreement 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

  1. What would a permitted control detect that denied requests alone would miss?
  2. Which request conditions must be valid to isolate an ownership check?
  3. Why is the stored value needed in addition to a 403 assertion?
  4. Which kind of check observes real browser cookie behavior, and why can a pure policy test not replace it?