Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/C2.13.mp4Object-level authorisation is assumed correct because the endpoint list was covered, and BOLA lives in the cells nobody enumerated.
A roles-by-objects-by-verbs matrix with every cell marked tested, assumed or unreachable, and the assumed cells ranked by blast radius.
Fill the matrix for CyberTravels with three roles and find the untested cells that matter.
The engagement touched every endpoint and called itself complete. Authorisation does not live in endpoints — it lives in cells, one per role per object per verb — and the two that would have mattered here were never sent a single request.
pentest.Matrix — roles by objects by verbs, every cell tested, assumed or unreachable. assumed means nobody looked, and it is exactly where rows 1, 2 and 4 of LABELS.md have been sitting while the endpoint list showed full coverage.Why endpoint coverage is the wrong denominator for object-level authorisation, and the grid that is the right one: roles by objects by verbs, with the untested cells ranked by blast radius rather than listed.
Object-level authorisation is assumed correct because the endpoint list was covered, and broken object access lives in the cells nobody enumerated.
Fill a roles-by-objects-by-verbs matrix from one credential per role, and mark every cell tested, mismatched or untested.
Cell coverage, not endpoint coverage: 12 of 30 cells on the sample, with two mismatches and the two highest-cost cells never tested.
Measure this
| what to measure | denominator or target |
|---|---|
| authorisation cell coverage, not endpoint coverage | 12 of 30 cells on the sample |
| mismatches, and untested cells ranked by blast radius | count, and the two highest-cost cells |
authorisation is a CELL, not an endpoint
booking(own) booking(other) refund audit_log
traveller ok ALLOW(!) deny ?
agent-svc ok ? allow ?
finance ok allow allow allow
ALLOW(!) = design says deny, estate says allow -> finding
? = never tested. ranked by blast radius, not schema order.
endpoint coverage: 100%. cell coverage: 40%.
the two audit_log cells nobody sent a request to are the ? that matter.Grey box is the mode that can actually find broken object-level authorisation, because it holds more than one identity. It is also the mode most often reported wrong, because the engagement touches every endpoint, calls itself complete, and never notices that authorisation is a property of the cell — this role, this object, this verb — and not of the endpoint.
The matrix is the whole method. Roles down one axis, objects and verbs across the other, and every cell in one of three states: tested, with what came back; mismatched, where the estate disagrees with the design; or untested, which is data rather than a gap to hide.
Two numbers fall out that an endpoint-coverage report cannot produce. The mismatches are the findings. The untested cells, ranked by what a wrong answer would cost rather than by the order the endpoints appear in the schema, are the backlog — and the coverage fraction is what stops "we tested everything" standing when 40% of the cells were never sent a request.
One credential per role is the minimum and the enabling fact. With one account you can test that a feature works; the second account is what turns a functionality test into a security test.
The skill fills in a grid: every role, against every object, against every verb. Then it compares the grid against what the design says should happen.
It flags the cells where the running estate disagrees with the design.
Then it ranks the cells nobody tested by blast radius — by how much damage each one could do — rather than in the order they happen to appear in the schema.
The skill — skills/redteam/greybox-authorization-matrix/SKILL.md
name: greybox-authorization-matrix
description: >-
With one credential per role and a schema but no source, fill a
roles-by-objects-by-verbs matrix, flag design mismatches, and rank the cells
nobody tested by blast radius. Use for a grey-box engagement, or when
endpoint coverage is being reported as authorisation coverage.
allowed-tools: Read, Grep, GlobGrey box is the mode that can actually find broken object-level authorisation, because it has more than one identity. It is also the mode most often reported wrong: the engagement touches every endpoint, calls itself complete, and never notices that authorisation is a property of the cell — this role, this object, this verb — not of the endpoint.
The matrix makes the untested cells visible, which is the whole point.
An engagement with low-privilege accounts and a schema, a BOLA/BFLA hunt, and any report whose coverage claim is a count of endpoints.
1 — List what you were handed. One credential per role is the minimum and the enabling fact; also the schema, a couple of ids you own and a couple you must not, the matrix as designed, and the scope so the run is not an incident.
2 — Build the full matrix. Roles × objects × verbs. Write down what the design says each cell should be, before touching anything.
3 — Exercise cells and record what came back. Every cell you do not exercise stays untested — that is data, not a gap to hide.
4 — Report three things. Mismatches, where the estate disagrees with the design. Untested cells, ranked by what a wrong answer would cost, not by schema order. And the coverage fraction, so "we tested everything" cannot stand when 40% of the cells were never sent a request.
tested 12 of 30 cells (40%)
2 mismatch(es):
! traveller can read booking(other): designed deny, observed allow
The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.
{
"data_sources": [{"source": "str", "gives": "str"}],
"cells": [{"role": "str", "object": "str", "verb": "str",
"design": "allow|deny", "observed": "allow|deny|null"}],
"mismatches": [{"role": "str", "object": "str", "verb": "str",
"design": "str", "observed": "str"}],
"untested": [{"role": "str", "object": "str", "verb": "str", "blast": 0}],
"coverage": 0.0
}
Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.
The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.
Install it, once. From the folder you cloned. --all does every agent it finds on your machine:
python3 scripts/install_skills.py --all --lessons C2.13Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick c2-13-grey-box-agentic-pentest:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons C2.13 | type /c2-13-grey-box-agentic-pentest |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons C2.13 | type /c2-13-grey-box-agentic-pentest in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons C2.13 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons C2.13 | type $c2-13-grey-box-agentic-pentest, or run /skills |
No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.
python3 scripts/lesson.py C2.13Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.
python3 scripts/checkpoint.py --at C2.13 --out work/cybertravels
python3 scripts/checkpoint.py --at C2.13 --diffOn Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.
Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.
Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.
If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.
The engagement exercises 12 of 30 cells (40%) and finds two mismatches — a traveller reading another traveller's booking and the agent service writing one — while the two highest-cost cells, both on the audit log, were never sent a request. Endpoint coverage would have called this complete.
Build the matrix for one service you own with two real accounts. The cell you least want to test — an admin verb on another tenant's object — is the one whose result you most need to know.
Tools used
Skill source: skills/redteam/greybox-authorization-matrix