LLM-powered · Phase 9.2

LLM Business Logic Scanner

A pattern matcher cannot know that your checkout should ignore a price sent by the client. This scanner hands your source and your endpoint list to a model, asks it to write attack plans as plain HTTP requests, runs those requests itself with two real logins, and asks a model whether the responses prove the flaw.

LLMRuns against the live appHighCriticalBusiness logicneeds two sessions

Four phases: two are code, two are the model

The scanner code picks the files and sends every request. The model is used twice: once to plan, once to judge. It never touches the network itself, and the scanner never decides on its own that an attack worked.

Runs only with: repo snapshot, discovered endpoints, target URL, a logged-in admin and regular user, scan mode authenticated or full, and an LLM client. 1 Select filesscanner code 2 Plan attacksmodel · primary client 3 Execute stepsscanner code · httpx 4 Judge resultsmodel · judgment client Scanner code: picks the files, sends every HTTP request, records status and body. Model: writes the attack plans and judges the outcome. It never sends a request. Both model calls use the same shared client, so token use is counted in the scan's cost report. keep .ts .tsx .js .jsx .py .go .rs path holds router, route, checkout, payment, order, webhook, cart, invoice ... skip node_modules, test, .spec. , max 30 files INwhole source of each file INup to 50 METHOD url lines INa framework guess OUTJSON attack_plans up to 10 plans up to 8 steps each each step: method, url, body sent as admin_user, regular_user or no_auth 15 s timeout per request keeps the status and the first 300 chars of the body INplan + success criteria each step: status, body[:300] OUTconfirmed true or false confidence, severity, evidence, remediation false: no finding Finding, only if confirmedsource: SAST-guided DAST Confidence is the judge's value, capped at 1.0 (0.7 if it gave none). Severity is the judge's; the plan's file and line become the code location.

1Four phases. The two shaded boxes call a model; the other two are ordinary code. Nothing runs unless two logged-in users, the endpoint list and an LLM client all exist, so a scan without an API key never reaches this phase.

2Select. The scanner keeps source files whose path contains words like checkout, payment, order, webhook or cart, drops tests and node_modules, and stops at 30 files. Each file is kept whole: the size limit is switched off.

3Plan. One call. The model gets the file contents, up to 50 discovered endpoints and a framework guess, and must answer with JSON: attack plans, each a list of HTTP steps plus a success criterion. The prompt tells it to use real endpoint URLs from the list.

4Execute. The scanner, not the model, sends each step, with the bearer token of the user the plan names. For every step it records the status and the first 300 characters of the body.

5Judge. A second call, on the cheaper judgment client when one is configured, receives the plan and what actually came back. It answers confirmed or not, with evidence, severity and a fix.

6Only confirmed: true becomes a finding. A plan that errors, parses badly or is judged not confirmed leaves no finding in the report; refused steps and failed plans are logged, emitted as progress messages and kept in the scanner's skipped_steps.

One plan, start to finish

An illustrative plan for a checkout route that trusts a price from the request body. The shapes are the real ones from the prompts; the endpoint, values and file are made up for the picture.

1 · plan from the model 2 · live app, sent by the scanner 3 · verdict from the judge "title": "Client-set price" "affected_file": "api/checkout/route.ts" "steps": [ {"user":"admin_user", "GET /api/products/7"}, {"user":"regular_user", "POST /api/checkout", "body":{"id":7,"price":1}}] "success_criteria": "201 and total is 1" Setup step as admin_user, attack step as regular_user. step 1 · admin_user GET /api/products/7 200 {"id":7,"price":40} step 2 · regular_user POST /api/checkout {"id":7,"price":1} 201 {"total":1,"paid":true} 201 {"total":40,"paid":true} The scanner stores status and the first 300 characters of each body, nothing more. sent to the judge plan, success_criteria, steps[].expected, steps[].actual_status, steps[].actual_body_preview The judge cannot send anything. It only reads what came back. judge replies "confirmed": true "confidence": 0.9 "severity": "HIGH" "evidence": "total 1 for a 40 product" + remediation text same plan, fixed server "confirmed": false The price was ignored: the total is the catalog price. No finding is created.

1The model reads the checkout route and writes a plan: a setup request as the admin, then the attack request as the regular user, plus a success criterion in words. It also names the file and line it suspects.

2The scanner runs step 1 with the admin's token. The response gives a baseline: product 7 costs 40.

3Step 2 runs as the regular user with a client-chosen price of 1. The server answers 201 and charges 1. The scanner has only recorded this so far; no verdict exists yet.

4The plan, its success criterion and the recorded responses go to the judge. Each body is cut to 300 characters before it is sent.

5The judge answers confirmed: true and quotes the evidence. This becomes a HIGH finding on POST /api/checkout, located at the file and line from the plan.

6Against a fixed server the same plan returns the catalog price. The judge answers confirmed: false and the plan is dropped without a finding.

What guards the output

Model output is untrusted text at both ends. Four gates stand between it and a finding. The fifth box is what the code does not check, which is why the judge's verdict is the only proof.

before the model plan reply while executing judge reply left open Preconditions mode authenticated/full two sessions: A and B endpoints and target URL repo snapshot, LLM client else the phase is skipped. No key, no code reviewer, so nothing here starts. Plan parse reply read as JSON; code fences are stripped first a plan without steps or not an object: dropped unparseable: zero plans, so nothing is sent to the app. Bounds 30 files, 50 endpoints 10 plans, 8 steps each 15 s per request failed request: status 0 time budget: stop early and return the findings found so far. Verdict must say confirmed: true false, bad JSON or any error: no finding confidence capped at 1.0 the judge reads 300 chars per body, so a long reply is judged on its start. Not guarded the verdict is the proof; no code re-reads the body off-origin step URLs are refused, never sent DELETE can be sent, with real tokens (target only) No API key: the primary LLM client is missing, no code reviewer exists, and this phase never starts. Rule-based scanners are unaffected. Unlike the rule-based scanners, a finding here is as sound as the model's judgment of 300 characters of response.

1Before any call. All preconditions must hold: scan mode, both logins, endpoints, target URL, repository and an LLM client. Without an API key the phase is simply absent.

2The plan reply. Parsed as JSON, with code fences stripped and a fallback that hunts for the first {...} block. A plan that is not an object or has no steps is dropped, and a malformed step inside a plan is skipped. If nothing parses, no request is sent.

3While executing. Hard caps on files, endpoints, plans and steps. A request that fails is recorded as status 0 and the plan continues. A cooperative time budget stops the loop before the runner's 30 minute cancel would discard everything.

4The verdict. Only confirmed: true passes. Anything else, including an exception, gives no finding. Evidence and response preview are cut to 300 characters.

5What the code does not check. The judge's word is final: no code re-reads the body. Every step URL is resolved against the target and refused (logged, counted, no request, no token) if its scheme, host or port differs; redirects off the target origin are not followed, and a malformed step is skipped without aborting the scan. Methods are not restricted, so DELETE against the target itself is still possible. Read confirmed findings as strong leads.

Why it matters

Why it matters

The flaws this finds depend on what your app is for: a price the client may set, a workflow step that can be skipped, a webhook with no signature check, a tenant id nobody scopes by. They have no signature a regex could match. The planner prompt covers seven classes: authorization bypass, state machine violations, price manipulation, race conditions, cross-tenant access, webhook replay or forgery, and mass assignment. Because the plans run against the live app, a flaw that looks plausible in code is either shown in a response or discarded.

What leaves your machine

The model receives the full text of up to 30 source files and up to 50 endpoint lines, then the plan with the recorded status and a 300 character preview of each response. Those previews come from the live app under the admin and regular sessions, so they can contain real records. Use a test environment and test accounts, as for the other authenticated scanners.

Vulnerable vs fixed code

The shape of flaw a plan like the one above proves, and the fix. The server must derive the price itself.

// BAD: the total comes from the request body
export async function POST(request: Request) {
  const { id, price } = await request.json()
  const order = await db.insert("orders", { productId: id, total: price })
  return NextResponse.json({ total: order.total, paid: true })
}

Limits and configuration

SettingValueNote
Files sent to the planner30path must contain a business-logic word; extensions .ts .tsx .js .jsx .py .go .rs; whole file, no truncation
Endpoints in the prompt50as METHOD url lines
Plans · steps10 · 8asked for in the prompt and enforced by slicing in code
Step URLsresolved against the target; scheme, host and port must matchoff-origin steps are refused with no request and no token; up to 5 same-origin redirect hops are followed and re-checked, and a redirect that leaves the origin is not followed
Request timeout15 sa failure becomes status 0 and the plan carries on
Body preview300 charswhat the judge sees per step, and the cap on stored evidence
Token limits20,480 · 2,048planner reply · judge reply
Plans runone after anotherthe config declares MAX_CONCURRENT_PLANS = 3, but the loop awaits each plan in turn
Wall clock30 min runner timeoutthe scanner stops cooperatively shortly before it
Reported ascategory Auth weakness · source SAST-guided DASTseverity and confidence come from the judge