How does ZAP's active scan engine turn its input-vector setting into the parameters a rule attacks?
answer
- one factory turns a mask into objects
- a list per message, not per scan
- empty list means skipped, not failed
- only per-parameter rules ever ask
basics
~20 sVariantFactory.createVariants reads the injectable bitmask and the enabled-RPC bitmask and returns a list of Variant objects for that one message. Each Variant exposes its own parameter list, and a parameter-based rule loops over every variant and every parameter.
solid answer
~40 sCore's `VariantFactory.createVariants(ScannerParam, HttpMessage)` is the translation step. It reads two bitmasks — the injectable set and the enabled-RPC set — and returns a `List<Variant>`; a `Variant` is a small adapter that knows how to pull named parameters out of one part of a message and write a value back. `AbstractAppParamPlugin.scan()` builds that list per message, then loops variants, then loops each variant's `getParamList()`. Two consequences matter in a pipeline. If the list comes back empty the rule is not run at all — it is marked skipped with the reason `no input vectors enabled`, and the scan still finishes normally. And only rules extending `AbstractAppParamPlugin` ever ask for the list, so vectors say nothing about rules that work per host or per page.
code
yaml · 7 linesinputVectors:
postData:
enabled: true
json:
enabled: true
scanNullValues: true
xml: falsego deeper
Know the name of the step: a factory turns the input-vector setting into a list of variants, and each variant offers the parameters a rule may change.
Explain the nesting — body encodings sit under post data, header scanning has a second condition — and say what happens when the list is empty.
Use it diagnostically: when a rule reports nothing, decide whether it was skipped for lack of vectors, handed no parameters, or never consults vectors at all.
Decide what the pipeline records. If a skipped rule looks identical to a clean rule in your report format, the format is the defect.
## From two bitmasks to a list of objects The input-vector setting is an integer. Rules do not read integers; they read parameters. The step between the two is core's `VariantFactory`, in `org.zaproxy.zap.extension.ascan`: ```java List<Variant> createVariants(ScannerParam scanOptions, HttpMessage message) ``` It reads **two** masks off the scan options — `getTargetParamsInjectable()` (which request parts) and `getTargetParamsEnabledRPC()` (which body encodings) — and returns a fresh list for that one message. ## What a Variant is A `Variant` is a small adapter over one part of a message. Its contract is three methods that matter here: - `setMessage(HttpMessage)` — parse this message and remember what you found; - `getParamList()` — the named values you are offering as attackable, each tagged with a type; - `setParameter(msg, pair, name, value)` — write a new value back into your part of the message. That is the whole abstraction. A rule never knows whether it is editing a query parameter, a JSON field or a cookie; it asks the variant for pairs and hands the variant a replacement value. ## The tree of gates The masks are not flat — some vectors unlock sub-vectors, and this is where a plan's YAML shape comes from. | `inputVectors` key | what the factory adds | note | |---|---|---| | `urlQueryStringAndDataDrivenNodes.enabled` | the query variant, plus OData query variants when `odata` is on | also adds the data-driven-node path variant, but only while `urlPath` is off | | `postData.enabled` | the form-body variant | the gate for everything below it | | `postData.json`, `xml`, `multiPartFormData`, `googleWebToolkit`, `directWebRemoting` | one body-encoding variant each | each is an enabled-RPC bit, and each is dead unless `postData.enabled` is true | | `httpHeaders.enabled` | the header variant | with a second condition — see below | | `cookieData.enabled` | the cookie variant | `encodeCookieValues` decides whether values are URL-encoded | | `urlPath` | the path-segment variant | every segment becomes a parameter | | `scripts` | one variant per enabled input-vector script | a script can offer any parameters it likes | ## The two gates people trip over 1. **Header scanning has a second condition.** With `httpHeaders.enabled` true but `allRequests` false, the engine adds the header variant *only* if the message already carries a query string or a body. The reasoning is that a page with no parameters is probably static. So a pure `GET` with no query gets no header testing until `allRequests` is also true. 2. **The body-encoding switches live under post data.** Turning JSON scanning on while `postData.enabled` is false achieves nothing, because the whole block of encoding variants is inside the post-data branch. ## What a rule does with the list The loop is worth knowing because it is where the request volume of a scan comes from: 1. build the variant list for this message; 2. for each variant, hand it a copy of the message and ask for its parameter list; 3. for each parameter, take a **fresh** copy of the message, ask the variant to write the attack value into it, and send that copy. Every attempt starts from a clean copy, so two parameters are never mutated at once, and a rule never has to know which part of the request it just edited. That is also why adding a vector multiplies work rather than adding to it: the new variant's parameters are attacked by *every* enabled parameter-based rule. ## When the list comes back empty `AbstractAppParamPlugin.scan()` calls the factory first, and if the returned list is empty it calls `pluginSkipped` with the message `no input vectors enabled` and returns. The rule raises nothing, the scan continues, and the run finishes in the ordinary way. **A misconfigured vector set does not fail a scan; it quietly empties it**, which is why a vector-set line in a report matters more than it looks. The same is true one level down: a variant that finds nothing to offer in this particular message — a cookie variant on a request with no cookies — simply returns an empty parameter list and costs nothing. ## Which rules this reaches at all In shipped code there is exactly **one** caller of `createVariants`, and it is the per-parameter rule base class. A rule written against the per-page or per-host base classes never asks for a variant list, so the input-vector setting has no bearing on what it does; those rules send their own requests against a page or a host. When you tell a developer "the scan does not cover cookies", the honest form is "no parameter-based rule was handed the cookie as a parameter", not "no rule looked at the cookie at all".
- Why is the variant list rebuilt for every message rather than once per scan?Because two variants decide by inspecting the message. The header variant checks whether this request carries a query or a body before offering itself, and the data-driven-node variant reads this URL's path against the site tree. A per-scan list could not make either decision.
- What does a rule do with a parameter once a variant hands it over?It asks the variant to write a replacement value into a fresh copy of the message and sends that copy, then judges the response. The rule never edits the request itself, which is why adding a variant is enough to extend every parameter-based rule at once.
saying these in an interview costs you the question
- Thinks an empty vector set makes the active scan fail loudly
- Believes turning on JSON scanning works without post data enabled
- Says every active rule is affected by the input-vector setting
- Expects header testing on a request with no parameters by default
- Describes a Variant as a scan rule rather than a parameter adapter