Why does a ZAP plan's activeScan-config job undo input vectors you set with -config at startup?
answer
- it seeds from constants, not current state
- absence is a reset, not a no-op
- both masks written unconditionally
- no key exists for the plain body
basics
~20 sThat job does not patch the current setting. It rebuilds the injectable bitmask from TARGET_INJECTABLE_DEFAULT, sets bits only for the blocks the YAML names, and writes the whole mask back, discarding whatever a startup config key had put there.
solid answer
~40 s`ActiveScanConfigJob.applyParameters` starts from the constants, not from the live value: it builds a bit set from `TARGET_INJECTABLE_DEFAULT` and one from `TARGET_ENABLED_RPC_DEFAULT`, flips bits for each `inputVectors` sub-block that is present, then calls `setTargetParamsInjectable` and `setTargetParamsEnabledRPC` unconditionally. The add-on's own unit test pins that behaviour: a job carrying `parameters` and **no** `inputVectors` block still writes exactly the two defaults. So `-config scanner.injectable=...` is honoured right up to the moment the job runs, then overwritten. The practical rule is that the job is a full statement of the vector set, not a patch on top of one — put every vector you need inside the block, and treat the block's absence as a reset rather than as leaving things alone.
code
yaml · 5 lines- type: activeScan-config
parameters:
defaultPolicy: Default Policy
# No inputVectors block: the injectable set is still
# rewritten, back to the built-in default.go deeper
Remember that the plan wins over the command line here, and that leaving a vector out of the block is not the same as leaving it alone.
Explain the mechanism: the masks are seeded from constants, bits are flipped for the blocks present, and both are written back every time.
Diagnose it. When a vector you configured is not active, check the plan for a config job before you suspect the rules, and read the option back over the API.
Set the convention: vectors are declared in the plan and nowhere else, so that one artefact under review describes the scan's reach.
## What the job does when it applies The `activeScan-config` job is the automation add-on's front end onto core's active-scan options. When the plan reaches it, `applyParameters` runs, and the first two lines of that method decide everything: ```java BitSet targetParamsInjectable = fromInt(ScannerParam.TARGET_INJECTABLE_DEFAULT); BitSet targetParamsEnabledRPC = fromInt(ScannerParam.TARGET_ENABLED_RPC_DEFAULT); ``` It seeds from the **compiled-in constants**. It never reads `getTargetParamsInjectable()` — the value in force a moment earlier — so nothing that set it before is carried forward. ## The sequence, in the order it happens 1. ZAP starts; `ScannerParam` loads `scanner.injectable` from the configuration, including any `-config scanner.injectable=...` you passed on the command line. At this point your value is live and correct. 2. The plan runs. It reaches the `activeScan-config` job, which seeds two bit sets from the defaults. 3. For each `inputVectors` **sub-block present in the YAML**, the job sets the matching bit from that block's `enabled` flag. Sub-blocks you left out keep their seeded default bit. The two scalar keys, `urlPath` and `scripts`, are written either way. 4. Both masks are written back with `setTargetParamsInjectable` and `setTargetParamsEnabledRPC`, unconditionally, whether or not the YAML had an `inputVectors` block at all. 5. A later `activeScan` job reads the result. Your startup value is gone. The behaviour is not incidental — the add-on's unit test asserts it directly, verifying that a job with `parameters` only writes precisely `TARGET_INJECTABLE_DEFAULT` and `TARGET_ENABLED_RPC_DEFAULT`. ## It is not only the vectors The same shape applies to the job's ordinary `parameters`. They are copied onto the live options by a helper that walks **every** getter on the job's own parameter object and writes each non-null value through the matching setter. Those fields are initialised in Java, not left null, so a job that names none of them still writes all of them: anti-CSRF token handling back to its default, the rule and scan duration limits back to unlimited, the alert cap back to unlimited, the thread count back to the computed default. **An `activeScan-config` job is a complete statement of the active-scan configuration, not a patch on top of one** — and that is the single sentence to carry out of this question. ## Three consequences worth carrying - **The block is a complete statement.** If you enable `cookieData` and say nothing else, the other vectors are not "left as they were"; they are whatever the shipped default says. Usually that is what you wanted, and occasionally it silently discards a deliberate setting. - **Two sub-keys are not optional.** `urlPath` and `scripts` are plain booleans on the block rather than nested objects with an `enabled` flag, and the job writes both with no presence check. `scripts` defaults to true, `urlPath` to false, on every application. - **Later jobs win.** Two `activeScan-config` jobs in one plan are not merged; each rebuilds the masks from the defaults, so the last one to run is the one that describes the scan. ## What the block cannot say at all | vector | reachable from `inputVectors`? | how to change it | |---|---|---| | query string, post data and its encodings | yes | the matching sub-block | | cookies, headers, URL path | yes | the matching sub-block | | input-vector scripts | yes | the `scripts` boolean | | **a plain body** | **no key exists** | the config key, the desktop options, or the control API | | user-defined injection points | no key exists | the config key, or the desktop dialog | The plain-body gap is the one to remember, because that bit **is** in the shipped default. The job seeds it on and has no key that could write it off, so **a plan cannot disable plain-body injection — and any `activeScan-config` job in the plan turns it back on** if a startup config key had disabled it. It is a small vector, offering the whole body as one value and only for a request with no content type or a `text/plain` one, but it is a vector the plan schema cannot speak about. ## How to write the job so it says what you mean Treat `inputVectors` the way you would treat a declarative manifest rather than a patch. State the vectors you want on and the ones you want off, in the plan, and stop passing `-config scanner.injectable=...` alongside it — a mask expressed as an integer is unreadable in review anyway, and it loses to the job. If you need the vector set to differ between two runs, that is two plans or one environment substitution, not a command-line override layered on top.
- How would you prove this behaviour on a running instance without reading the source?Read the option back. The control API exposes the injectable mask as an `ascan` option view, so query it after startup with your config key applied, run the plan, and query it again. The two values differ whenever an activeScan-config job ran.
- Does the same reset apply to the job's other parameters, such as the thread count?Yes, and that surprises people. The parameters are copied by a helper that walks every getter on the job's own parameter object and writes each non-null value. The fields are initialised in Java, so omitting a key writes the job's default rather than preserving what was set before.
- What happens if two activeScan-config jobs appear in one plan?Each one rebuilds both masks from the defaults when it applies, so they do not accumulate. The one that runs last decides the vector set for every activeScan job after it, which makes plan order a functional detail rather than a cosmetic one.
saying these in an interview costs you the question
- Thinks the job merges its block into the current vector set
- Expects a startup config key to survive the plan
- Assumes an absent inputVectors block leaves vectors untouched
- Believes two config jobs in one plan accumulate their settings
- Looks for a plainBody key in the inputVectors block