skip to content

Why can installing a ZAP add-on add an input vector that no plan setting switches off?

level: seniorimportance: should knowfreq 40%

answer

  1. two phases, only one is gated
  2. the hook registers a class, not a bit
  3. appended last, after every check
  4. check getParamList before you claim coverage

basics

~20 s

An add-on registers a Variant class through its extension hook, and VariantFactory appends every registered Variant at the end of createVariants, after all the bitmask checks. Nothing in the inputVectors block or the policy gates that path.

solid answer

~40 s

There are two ways a `Variant` reaches the list, and only one of them is gated. The built-in variants are added inside `if` branches that test the injectable and enabled-RPC bitmasks. Add-on variants take a different route: the add-on calls `ExtensionHook.addVariant(Class)`, the extension loader passes it to `VariantFactory.addVariant`, and `createVariants` appends all of them at the very end, **outside every bitmask check**. So installing the `graphql` or `grpc` add-on genuinely widens what parameter-based rules attack, and no `inputVectors` key or scan policy can switch it off — removing the add-on is the control. It is worth checking which ones matter: `soap`, `openapi` and `mcp` also register a variant, but theirs return an empty parameter list and exist only to shape the site tree.

go deeper

for a junior

Know that add-ons can add input vectors, and that the list of vectors in the plan is therefore not the whole list for a run.

for a middle

Trace the route: extension hook, extension loader, factory, then appended after every bitmask check, which is why no key gates it.

for a senior

Use it when two runs disagree. Compare add-on sets, not just plans, and check whether the suspect variant actually returns parameters.

for a principal

Treat the image's add-on set as part of the scan contract, pinned and reviewed like the plan, because it silently changes what the run is permitted to touch.

## Two routes into the same list `VariantFactory.createVariants` builds its list in two distinct phases, and the difference between them is the answer to this question. 1. **The gated phase.** Every built-in variant is added inside a branch that tests a bit: `if ((targets & TARGET_COOKIE) != 0)`, `if ((enabledRPC & RPC_JSON) != 0)`, and so on. These are the vectors a plan's `inputVectors` block controls, because the block writes those bits. 2. **The ungated phase.** The last thing the method does before returning is call `addCustomVariants(listVariant)`, which instantiates and appends **every** class an extension has registered. There is no bit, no key and no condition in front of it. ## How a class gets into the ungated phase The route is short and worth being able to name in order: - the add-on's extension calls `extensionHook.addVariant(VariantX.class)` while it is being hooked; - core's extension loader walks those registrations, constructs one instance as a sanity check, and calls `VariantFactory.addVariant(Class)`; - the factory keeps them in a `customVariants` list for the life of the installed add-on; - every subsequent `createVariants` call appends all of them. The registration is by **class**, not by instance: the factory constructs a fresh object per message, which is why a variant can keep per-message parsing state. ## Not every registered Variant is an attack surface This is where an overstatement is easy, so check the class rather than the add-on name. A `Variant` has two jobs — offering parameters, and shaping how a message is filed in the site tree — and some add-ons register one purely for the second. | add-on | what its variant offers | effect on what is attacked | |---|---|---| | `graphql` | inline argument values pulled out of a query document | real new parameters, ungated | | `grpc` | fields decoded out of a binary message body | real new parameters, ungated | | `soap` | an empty parameter list | none — it only supplies a tree path | | `openapi` | an empty parameter list | none — it only supplies a tree path | | `mcp` | an empty parameter list from `getParamList` | none — it only supplies a tree path | So the honest sentence is *"installing an add-on **can** add an input vector the plan does not control"*, and the way to tell is whether that add-on's variant returns anything from `getParamList`. ## How to check one for yourself You do not need to guess, and the check is the same every time: - find the extension class that calls `addVariant` and note the class it registers; - read that class's `getParamList`. An empty list means no new attack surface at all; - if it does return pairs, look at the type they are tagged with, because that type is what the per-parameter exclusions later test against. The registration also has a lifetime: variants added through an extension hook are removed from the factory when that extension is unloaded, so the vector disappears with the add-on rather than persisting in the session. ## The deliberate counter-example: variant scripts Input-vector **scripts** go the other way, and the contrast makes the design visible. A script that implements the variant script type is added inside a bitmask check — the custom-RPC bit, exposed in a plan as the `inputVectors.scripts` key, which defaults to on. So: - a script you wrote is **gated** and can be switched off from the plan; - a compiled variant shipped by an installed add-on is **not**. If you need the second one off, the control is the add-on set, not the plan. ## Why a pipeline owner cares - **Coverage moves when the add-on set moves.** Two runs of the same plan against the same target can attack different things because the images differ. The vector set is not fully described by the plan file. - **The blast radius moves too.** A body an untouched scan would have replayed verbatim becomes a body the run mutates field by field. That is more traffic, differently shaped, against a system you were permitted to test on a stated basis. - **Reproducibility needs the add-on set recorded** next to the plan and the vector set. Without it, a finding that appears one week and not the next has no explanation in either artefact. - **Do not reach for a policy to disable it.** A policy names rules, strengths and thresholds. It has no idea a new variant exists.

  • If a plan cannot disable an add-on's variant, what can?
    The installed add-on set. Ship an image without that add-on, or unload the extension, and its registrations leave the factory with it. The automation job that once installed add-ons is a deprecated no-op, so the add-on set is decided when the image is built, not in the plan.
  • Does registering a Variant also change how requests are filed in the site tree?
    It can, and for several add-ons that is its only purpose. A variant may supply a tree path or a leaf name for a message, which merges or splits nodes. That path is used even when the variant offers no attackable parameters at all.

saying these in an interview costs you the question

  • Assumes every input vector is listed in the plan file
  • Thinks a scan policy can disable an add-on's variant
  • Claims any add-on registering a Variant widens the attack surface
  • Treats input-vector scripts and add-on variants as the same path
  • Ignores the installed add-on set when comparing two scan results