What start options do ZAP's packaged scan scripts impose on the process they launch?
answer
- one helper builds all three command lines
- it binds every interface
- the API key is switched off
- the address allow-list is a wildcard
basics
~20 sThey start ZAP bound to every interface, with the control API's key switched off and its address allow-list opened to any caller, on a port they pick themselves. Every packaged run therefore exposes an unauthenticated control API for its lifetime.
solid answer
~40 sAll three wrappers build their command line from one shared helper in `zap_common.py`, so the same options go on whichever script you call: `-host 0.0.0.0`, `-config api.disablekey=true`, `-config api.addrs.addr.name=.*` with `regex=true`, `-config database.recoverylog=false`, and a `-port` the script picks from the free ephemeral range unless you name one with `-P`. That same list is used for the `-daemon` process the wrapper drives over the API *and* for the one-shot `-cmd` run of a generated plan, and it is repeated when the script starts its own container — where the port is also published to the host. The practical consequence is that a packaged run stands up an unauthenticated control API reachable from any address for as long as the scan lasts.
code
bash · 8 lines# what every packaged run starts, whatever flags you passed
zap-x.sh -daemon \
-port "$PORT" \
-host 0.0.0.0 \
-config database.recoverylog=false \
-config api.disablekey=true \
-config api.addrs.addr.name=.* \
-config api.addrs.addr.regex=truego deeper
Remember that a packaged scan is not a self-contained program: it starts a ZAP process with a remote-control interface open, and that interface needs no credential.
Be able to name the options the shared helper always passes and say which of them affects reachability rather than performance.
Explain why the exposure is identical on both execution paths and what placement decisions actually contain it on a shared CI runner.
Own the standard for where these scans may run at all, and treat the open control surface as a property of the tool you design around rather than a flag someone forgot.
## One helper builds every command line The three packaged scan scripts do not each assemble their own invocation. They share `zap_common.py`, whose `create_start_options(mode, port, extra_params)` returns a fixed list and then appends whatever the calling script added. Two functions call it — `start_zap`, which launches a `-daemon` process the wrapper then drives, and `run_zap_inline`, which runs a one-shot `-cmd` process — and `start_docker_zap` repeats the same list when the script has to start a container itself. **No invocation of these scripts skips those options.** ## The options, and what each gives up | option the wrapper always passes | what it does | what it gives up | |---|---|---| | `-host 0.0.0.0` | binds the listener to every interface | reachability is no longer limited to loopback | | `-config api.disablekey=true` | switches off the control API's key | callers need no credential | | `-config api.addrs.addr.name=.*` plus `regex=true` | allows any calling address | the address allow-list stops filtering | | `-config database.recoverylog=false` | drops the session recovery log | throughput, at the cost of crash recovery | | `-port <chosen>` | a free port the script picks unless `-P` names one | predictability, not exposure | The port is not a hardening measure. The script asks the operating system for a free ephemeral port before it starts anything and then points its own client at that same port, so "it is a random port" buys obscurity and nothing else. ## Why the plan path is no different It is tempting to assume that the `--auto` path, which runs a generated plan with `-cmd` instead of driving the process over the API, does not need the API open and therefore does not open it. It does open it. `run_zap_inline` calls the same `create_start_options`, so a one-shot plan run is started with the same broad binding and the same disabled key as the long-running daemon. The difference between the two paths is **lifetime and who issues the work**, not exposure. ## Where that leaves a pipeline For a nightly job the honest framing is: for the duration of the scan, the machine running it hosts an unauthenticated remote-control surface for a program whose purpose is to attack web applications. Anything that can route to the port can drive it. Practical consequences: - **placement is the control.** The wrapper's options are not configurable away, so containment has to come from where you run it — a network the job does not share, no published port beyond it, and a short process life; - **`docker run -p` makes it worse** — when the script starts its own container it publishes the chosen port to the host, so the surface is reachable from wherever the host is; - **shared CI runners are the sharp case.** A runner with several jobs on one network gives every other job on it a live control surface for the scan; - **`-z` appends, it does not replace.** Options you pass with `-z` are added after the fixed list, so the defaults are always sent; - unless you pass `-silent`, the scripts also send `-addonupdate` and install rule-set add-ons, so a packaged run reaches the marketplace before it reaches your target. ## Why it is built this way None of this is carelessness; it is the cost of a wrapper that has to work in two very different situations with one command line. The script may be running inside the published image, where the process it starts is the only thing in the container and the port is not reachable from anywhere else by default — or it may have been started from a host, in which case it launches a container and has to reach *into* it from outside. The second case is why the binding is broad and the key is off: a loopback binding inside the container would be unreachable from the script driving it, and a generated key would have to be passed back out. **The convenience was designed for the harder of the two cases and then applied to both**, which is exactly the shape of decision worth being able to narrate rather than just condemn. ## The mistake to avoid making out loud Saying "the scan runs on localhost so the API is fine" is the wrong answer twice: the binding is not loopback, and the key is off rather than generated. Saying "it is only up for a few minutes" concedes the point rather than answering it — the question is who can reach the port during those minutes, and the script's own options make that answer "anything that can route to it".
- Does the `--auto` plan path avoid the open control API?No. The plan path calls the same helper, so the one-shot `-cmd` process is started with the same `-host 0.0.0.0` and `api.disablekey=true` as the daemon the legacy path drives. What changes is the process lifetime and who issues the work, not the exposure.
- What can a pipeline actually do about it?Treat the scan process as untrusted infrastructure: give it a network nothing else shares, do not publish its port beyond that network, and keep its life short. The options are baked into the scripts, so containment has to come from placement.
- Why do the scripts choose a port instead of using a fixed one?So that several runs can share a machine without colliding: the script asks for a free ephemeral port, then points its own client at it. `-P` pins it when a pipeline needs a predictable port to route or publish.
saying these in an interview costs you the question
- The wrappers start ZAP on loopback only
- The control API still needs a key during a packaged scan
- Only the daemon path opens the API; the plan path does not
- The scripts always use the project's standard proxy port
- It is fine to leave the API open because the run is short