All lessons › Function C — Application Security with an AI SDLC › The AI SDLC — an Agentic AppSec Pipeline, Before and After Deploy
C2.9

Exploit chaining

C2
▶
Lightboard recording

Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.

Video placeholderpublishes automatically from recordings/C2.9.mp4
Risk

Three medium findings are triaged as three mediums, and nobody notices they compose.

Control

Stage 13: combine validated findings into multi-step sequences and score the chain, not the links.

The lab

Chain individually-medium findings into a critical path and show the severity the chain earns.

◉Use case relevance

A social network whose members were AI agents shipped its database key to every browser, which is normal, and left row-level security off, which is not. One query returned every agent's record — including the OpenAI, Anthropic and AWS keys of the people who created them, in plaintext.

At CyberTravels. Chain and chains() in findings.py. The chain in your tree reads as one sentence: a vendor filename reaches a reader that does not check ownership, in a process that can also reach a shell. Three mediums, scored as critical — and a recipe whose links are not all confirmed builds nothing, because a chain assembled from hypotheses is a story that will be read as a finding.

◴What this lesson is — Day 0, Day 1, Day 2

Composing confirmed findings into sequences and scoring the chain rather than the links, because a chain is invisible from inside a queue row — with the rule that keeps it a finding: every link confirmed, or what you have is a story.

Day 0 — why

Three medium findings are triaged as three mediums, and nobody notices they compose into a critical.

Day 1 — how

Combine validated findings into multi-step sequences where one's effect satisfies another's precondition.

Day 2 — measure

Chains discovered, and the severity of the chain against the highest severity of its links — the difference is what individual triage missed.

Measure this

what to measuredenominator or target
chains discoveredcount
chain severity against the highest severity of its linksdelta; the difference is what individual triage missed

▦The framework, and how it works

   browser                    Supabase Data API           agents table
   +-----------+              +-----------------+         +---------------+
   | anon key  | -----------> |  row-level      |  ...    | id            |
   | (by       |              |  security       |         | owner         |
   |  design)  |              |  DISABLED       | ------> | provider_key  |  <-
   +-----------+              +-----------------+         | claim_token   |
                                                          +---------------+
   the anon key was never the problem. the absent policy was.

   what leaked            who can revoke it
   session token          Moltbook
   claim token            Moltbook
   provider API key       the person who created the agent   <- not the platform

Stage 13 — Exploit chaining. Individual findings are triaged individually, and that is how three mediums become a critical nobody noticed.

The arithmetic of severity is not additive. A read-only information disclosure is a medium. A CSRF is a medium. An unauthenticated internal endpoint is a medium. Chained — leak an ID, forge a request using it, hit the internal endpoint with the forged session — the outcome is account takeover, which is not a medium.

The pipeline can find these mechanically because Phase 4 already produced confirmed findings with known preconditions and effects. If one finding's effect satisfies another's precondition, they compose, and the chain's severity is the severity of its final effect.

This is the stage that most often changes what gets fixed first.

Where you are in the pipeline. `` [Ingestion & Mapping] ──> [Threat Modelling] ──> [Discovery] └─ stages 1-4 └─ stages 5-6 └─ stages 7-10 ──> [Dynamic Validation] ──> [Reporting] └─ stages 11-14 └─ stage 15 ``

2 · Stage 13 — findings as preconditions and effects

3 · Compose them — an effect that satisfies the next precondition

4 · Where it breaks — triage the links, miss the chain

5 · Phase 4 as a skill — and the preconditions that gate it

Dynamic validation is the one phase that acts. Everything before it reads; this one sends input to a running system. The skill therefore opens with safety preconditions rather than a procedure, and a refusal is a first-class output.

The contract also insists that reproduced: false be reported rather than dropped. A finding that survived Phase 3 and then failed to reproduce is the most useful signal the pipeline produces about its own false-positive rate — and it is the one a tidy report deletes.

6 · Where it breaks — the tidy report

Now suppose two of these findings do not reproduce, and the pipeline does the natural thing with them.

▶Real time execution as skill

The skill — skills/appsec/appsec-exploit-validate/SKILL.md

name: appsec-exploit-validate
description: >-
  Prove a candidate vulnerability is real by reproducing it in an isolated
  sandbox, then decide whether it chains into something worse. Use when asked
  to validate or confirm a finding, write or run a proof-of-concept, check
  exploitability, reproduce a CVE, or separate theoretical findings from
  demonstrated ones.
allowed-tools: Bash, Read, Write, Grep

AppSec pipeline · Phase 4 — Dynamic validation

Covers stages 11–14. A finding that has been reproduced is a different object from a finding that has been argued for. This phase produces the difference, and it is the phase with the sharpest safety constraints.

When to use this

Only on findings that reached feasible: true, verdict: confirmed in Phase 3. Validating an unfiltered list wastes the most expensive stage in the pipeline on findings Phase 3 would have deleted.

Safety preconditions — check before anything else

Refuse to proceed unless all hold, and say which one failed:

  1. The target is a local sandbox or an explicitly authorised environment. Never a production host, never a third party's infrastructure.
  2. The sandbox has no credentials beyond throwaway ones, and no route to a real network the exploit could reach.
  3. The proof-of-concept demonstrates the flaw. It does not exfiltrate real data, persist, escalate beyond the demonstration, or damage anything.
  4. There is a teardown and it runs even on failure.

This is not ceremony. A validation harness with production credentials in its environment is itself the vulnerability.

Procedure

Stage 11 — Sandbox replication. Stand the component up in isolation with the smallest fixture that reproduces the conditions: the vulnerable version, the reachable entry point, and nothing else. Record the exact commit and the fixture, because a PoC that cannot be re-run is an anecdote.

Stage 12 — Dynamic exploitation. Drive the entry point with an input that should trigger the sink. Capture the observable: the crash, the query executed, the file read, the process spawned. The observable is the evidence — an exit code is not. Record what you sent and what came back.

Stage 13 — Exploit chaining. Ask what the demonstrated primitive gives access to next. A path traversal that reads a config file containing a token is not a file-read bug; it is a credential-disclosure bug. Chain only within the sandbox, and stop at the first step that would need a real credential.

Stage 14 — Remediation engineering. Propose the fix at the right layer, and say what it costs. Prefer the control that makes the class impossible (parameterised queries, an allowlist, a type) over the one that blocks the sample payload (a regex on the input you happened to send). Then re-run stage 12 against the fix — a remediation that has not been tested against the PoC that motivated it is a hypothesis.

Example

Input — the fixture committed at the top of scripts/appsec_exploit_validate.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.

Output — the opening lines of a real run:

id     cwe       sev      requires                              grants
------------------------------------------------------------------------------------------------
F-01   CWE-200   low      ['unauthenticated']                   ['valid_report_id']
F-02   CWE-639   medium   ['authenticated', 'valid_report_id']  ['other_users_report_data']
F-03   CWE-352   medium   ['authenticated']                     ['password_reset_for_victim']
F-04   CWE-89    high     ['authenticated']                     ['database_read']
F-05   CWE-306   medium   ['internal_network']                  ['admin_actions']
F-06   CWE-918   medium   ['authenticated']                     ['internal_network']

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.

Output contract

{
  "validations": [
    {"finding_id": "str", "reproduced": true,
     "sandbox": {"commit": "str", "fixture": "str", "isolated": true},
     "input": "str", "observable": "str",
     "chain": [{"primitive": "str", "leads_to": "str", "stopped_because": "str"}],
     "remediation": {"layer": "input|query|api|config|architecture",
                     "change": "str", "cost": "low|medium|high",
                     "retested": true, "still_reproduces": false}}
  ],
  "refused": [{"finding_id": "str", "precondition_failed": 1, "why": "str"}]
}

reproduced: false is a first-class result and must be reported, not dropped. A finding that survived Phase 3 and then failed to reproduce is the single most useful signal the pipeline produces about its own false-positive rate.

Failure modes

  • Reporting reproduced: true without an observable. The observable is the claim; without it there is nothing to check.
  • Treating "the exploit ran" as "the fix works". Re-run after remediation or set retested: false and say so.
  • Chaining outside the sandbox. Stop and record stopped_because.
  • Fixing the payload instead of the class. A regex that blocks ../ does not fix path traversal.

Handoff

Pass validations to appsec-triage-report. Findings that failed to reproduce keep their finding record and gain reproduced: false — the report must be able to distinguish "demonstrated" from "asserted".

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.

1In your AI agent — you are taught

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.9

Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.

Then pick c2-9-exploit-chaining:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons C2.9type /c2-9-exploit-chaining
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons C2.9type /c2-9-exploit-chaining in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons C2.9type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons C2.9type $c2-9-exploit-chaining, or run /skills

2As one Python command — you just run it

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.9

3Read the code, and the difference — nothing runs

Neither 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.9 --out work/cybertravels
python3 scripts/checkpoint.py --at C2.9 --diff

On 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.

✓What you just proved

Six confirmed findings compose into multiple chains. The highest individual severity is high while the highest chained severity is critical, and at least one critical chain is built entirely from medium-or-lower links — for example SSRF granting internal network access, then the unauthenticated admin endpoint. Remediation ordering puts a medium finding first because it breaks the most chains.

✎Your turn

Take your current open findings and write down each one's preconditions and effects. The chaining falls out mechanically, and the finding you should fix first is usually not the one at the top of the severity-sorted queue.

Tools used

OWASP ZAPKimi K2Claude Opus 5

Skill source: skills/appsec/appsec-exploit-validate

← PreviousC2.8 · Dynamic exploitation (DAST)