## Summary

A materially distinct alternate trigger was confirmed on the vulnerable ZAP ViewState add-on: a malicious `javax.faces.ViewState` value in a proxied HTTP **request body** reaches the same `JSFViewState.decode()` / `ObjectInputStream.readObject()` sink through the add-on's request-panel factory. This differs from the parent reproduction, which used a malicious web server response body and the response-panel factory. However, this is **not a bypass** of the version 4 fix. The fixed commit `ac6c3f94d38505bc0facea286a4d3728044c6e5c` disables the shared JSF ViewState registration in `ViewStateModel`, so both request and response panel paths stop before constructing `JSFViewState`.

## Fix Coverage / Assumptions

The fix relies on the invariant that all normal add-on paths to JSF ViewState deserialization pass through `ViewStateModel`'s constructor-populated `viewstateParams` list. The relevant patch comments out the only JSF parameter registration:

```java
// viewstateParams.add(new ViewState(null, JSFViewState.KEY, "javax.faces.ViewState"));
```

This explicitly covers both ViewState panel factories because they instantiate the same model constructor:

- `ExtensionHttpPanelViewStateView.RequestSplitBodyViewStateViewFactory.getNewView()` -> `new ViewStateModel(ViewStateModel.VS_ACTION_REQUEST, null)`
- `ExtensionHttpPanelViewStateView.ResponseSplitBodyViewStateViewFactory.getNewView()` -> `new ViewStateModel(ViewStateModel.VS_ACTION_RESPONSE, null)`

The fix does not remove the dangerous sink itself: `addOns/viewstate/src/main/java/org/zaproxy/zap/extension/viewstate/zap/utils/JSFViewState.java` still contains unrestricted `ObjectInputStream.readObject()`. Therefore, future code that re-registers JSF ViewState or directly instantiates `JSFViewState` on untrusted proxied HTTP data would reintroduce the same root cause. Within the current fixed add-on panel surfaces, no gap was found.

No `SECURITY.md` or explicit serialization threat-model document was present in the inspected `zap-extensions` repository. The repository identifies ZAP as an HTTP/HTTPS proxy for assessing web application security, so proxied HTTP peer/message data was treated as crossing the relevant trust boundary. Direct local calls to parser classes were not treated as vulnerability variants.

## Variant / Alternate Trigger

Tested alternate trigger:

- **Original parent surface:** malicious HTTP response body from an attacker-controlled server -> `ResponseSplitBodyViewStateViewFactory` -> response HTML hidden-input parsing -> `JSFViewState.decode()`.
- **Variant candidate surface:** proxied HTTP request body containing `javax.faces.ViewState=<serialized object>` -> `RequestSplitBodyViewStateViewFactory` -> request parameter parsing and URL decoding -> `JSFViewState.decode()`.

Exact entry point tested:

- Entry point kind: proxied HTTP request body as represented by a real ZAP `HttpMessage`.
- Request body: `application/x-www-form-urlencoded` body with `javax.faces.ViewState` set to a Base64 Java serialized object.
- Product component path:
  1. `ExtensionHttpPanelViewStateView$RequestSplitBodyViewStateViewFactory.getNewView()`
  2. `HttpPanelViewStateView`
  3. `DefaultHttpPanelViewModel.setMessage(HttpMessage)`
  4. `HttpPanelViewStateView.dataChanged()`
  5. `ViewStateModel.getData()`
  6. `ViewStateModel.getViewStateParam()` request branch (`StandardParameterParser` + `URLDecoder`)
  7. `JSFViewState.decode()`
  8. `ObjectInputStream.readObject()`

The variant was confirmed on vulnerable commit `7e686c0900e82bb73b57880c0328d012269f8741` and blocked on fixed commit `ac6c3f94d38505bc0facea286a4d3728044c6e5c`.

## Impact

- **Package/component affected:** `zap-extensions`, add-on `addOns/viewstate` / ZAP ViewState add-on.
- **Affected versions as tested:** vulnerable parent `7e686c0900e82bb73b57880c0328d012269f8741` / ViewState add-on before version 4.
- **Fixed version as tested:** commit `ac6c3f94d38505bc0facea286a4d3728044c6e5c` / ViewState add-on version 4.
- **Risk level and consequences on vulnerable versions:** High. An attacker who can cause ZAP to proxy a request carrying a malicious JSF ViewState value can trigger Java deserialization in the ZAP process when the ViewState request panel processes the message, resulting in command execution with the ZAP user's privileges.
- **Fixed-version result:** no RCE; the same request body was processed by the fixed request panel without JSF registration and without marker/RCE files.

## Impact Parity

- **Disclosed/claimed maximum impact for parent:** arbitrary code execution / RCE in ZAP through insecure JSF ViewState deserialization.
- **Reproduced impact from this variant run:** command execution on the vulnerable commit. The payload wrote `request_vuln_marker.txt` and `request_vuln_rce_output.txt`, including `RCE_VIA_ZAP_VIEWSTATE_REQUEST_VARIANT` and `id` output from the deserializing JVM.
- **Parity:** `full` for vulnerable-version alternate trigger; `none` on the fixed commit because the fix blocks the path.
- **Not demonstrated:** a full GUI browser workflow that auto-submits a malicious form through a live ZAP desktop session. The proof instead drives the actual built add-on request-panel component with a real ZAP `HttpMessage`, which is the relevant product component boundary for this variant analysis.

## Root Cause

The same root cause is reachable on vulnerable versions because `ViewStateModel` globally registered `javax.faces.ViewState` as JSF ViewState for both request and response models. In the request model (`VS_ACTION_REQUEST`), `getViewStateParam()` parses request parameters with `StandardParameterParser`, URL-decodes the matching value, constructs `new JSFViewState(decVal, vsp.getName())`, and `getData()` then calls `vs.getDecodedValue()`. For `JSFViewState`, this enters `JSFViewState.decode()` and calls unrestricted Java deserialization:

```java
ObjectInputStream ois = new ObjectInputStream(bais);
return (T) ois.readObject();
```

The fix commit is:

- `https://github.com/zaproxy/zap-extensions/commit/ac6c3f94d38505bc0facea286a4d3728044c6e5c`

That commit disables the shared JSF registration rather than hardening `JSFViewState.decode()`. Because both request and response panel models use the same registration list, this alternate request-body trigger is blocked by the fix and is not a fixed-version bypass.

## Reproduction Steps

1. Use `bundle/vuln_variant/reproduction_steps.sh`.
2. The script:
   - Resolves the vulnerable parent `7e686c0900e82bb73b57880c0328d012269f8741` and fixed commit `ac6c3f94d38505bc0facea286a4d3728044c6e5c`.
   - Builds the actual ViewState `.zap` add-on from both commits in isolated worktrees under `bundle/vuln_variant/`.
   - Compiles a Java harness that instantiates `ExtensionHttpPanelViewStateView$RequestSplitBodyViewStateViewFactory` from each built add-on.
   - Generates a Base64 Java serialized payload and places it in a URL-encoded request body parameter named `javax.faces.ViewState`.
   - Drives `DefaultHttpPanelViewModel.setMessage(HttpMessage)` on the request model.
   - Checks for marker/RCE output on the vulnerable build and confirms absence of those files on the fixed build.
3. Expected outcome:
   - Script exits `1` because no fixed-version bypass is present.
   - Vulnerable evidence shows `RequestSplitBodyViewStateViewFactory`, `ViewStateModel.has_jsf_registration=true`, `JSFViewState.decode`, and marker/RCE files.
   - Fixed evidence shows `ViewStateModel.has_jsf_registration=false`, no marker file, and no RCE output file.

## Evidence

Primary evidence files:

- `bundle/vuln_variant/reproduction_steps.sh` — stage-specific reproduction script, executed twice successfully/idempotently.
- `bundle/logs/vuln_variant_reproduction_steps.log` — full build and runtime transcript from the latest run.
- `bundle/logs/vuln_variant_request_vuln.log` — vulnerable request-panel runtime evidence.
- `bundle/logs/vuln_variant_request_fixed.log` — fixed request-panel negative-control runtime evidence.
- `bundle/logs/vuln_variant_build_vuln.log` and `bundle/logs/vuln_variant_build_fixed.log` — Gradle build logs.
- `bundle/vuln_variant/runtime_manifest.json` — runtime manifest for the variant candidate.
- `bundle/vuln_variant/artifacts/request_vuln_marker.txt` — vulnerable marker with stack trace through the same sink.
- `bundle/vuln_variant/artifacts/request_vuln_rce_output.txt` — command output from the vulnerable deserialization payload.
- `bundle/vuln_variant/artifacts/source_identity_request_variant.txt` — tested commits and built archive hashes.

Key vulnerable excerpts:

```text
[vuln] factory=org.zaproxy.zap.extension.viewstate.ExtensionHttpPanelViewStateView$RequestSplitBodyViewStateViewFactory
ViewStateModel.param name=javax.faces.ViewState type=JSF
ViewStateModel.has_jsf_registration=true
[vuln] marker_exists=true
[vuln] rce_output_exists=true
```

The vulnerable marker stack includes:

```text
DESERIALIZATION_RCE_CONFIRMED_REQUEST_VARIANT
RCE_VIA_ZAP_VIEWSTATE_REQUEST_VARIANT
  at org.zaproxy.zap.extension.viewstate.zap.utils.JSFViewState.decode(JSFViewState.java:92)
  at org.zaproxy.zap.extension.viewstate.ViewStateModel.getData(ViewStateModel.java:183)
  at org.zaproxy.zap.extension.viewstate.HttpPanelViewStateView.dataChanged(HttpPanelViewStateView.java:189)
  at org.zaproxy.zap.extension.httppanel.view.DefaultHttpPanelViewModel.setMessage(DefaultHttpPanelViewModel.java:42)
```

Key fixed excerpts:

```text
[fixed] factory=org.zaproxy.zap.extension.viewstate.ExtensionHttpPanelViewStateView$RequestSplitBodyViewStateViewFactory
ViewStateModel.param_count=2
ViewStateModel.param name=__VIEWSTATE type=ASP
ViewStateModel.param name=__VIEWSTATEFIELDCOUNT type=ASP
ViewStateModel.has_jsf_registration=false
[fixed] marker_exists=false
[fixed] rce_output_exists=false
[fixed] PASS: fixed request-panel did not deserialize malicious JSF ViewState
```

The default cached source was also inspected at commit `c639b4e3fe10e472cbe8542de7fbe3964b4d4ce7`; the `javax.faces.ViewState` registration remained commented out there.

## Recommendations / Next Steps

- Keep the version 4 fix that disables JSF support; it covers both request and response ViewState panel paths.
- Add regression tests for **both** `RequestSplitBodyViewStateViewFactory` and `ResponseSplitBodyViewStateViewFactory` that feed `javax.faces.ViewState` serialized-object-looking data and assert that `JSFViewState.decode()` is not reached.
- For defense in depth, remove `JSFViewState.decode()`'s unrestricted `ObjectInputStream.readObject()` implementation or guard it with a strict JEP 290 `ObjectInputFilter` and a very small allowlist if JSF parsing is ever restored.
- Audit future changes for any reintroduction of `new ViewState(... JSFViewState.KEY, "javax.faces.ViewState")` or direct use of `JSFViewState` on proxied HTTP data.

## Additional Notes

- Idempotency confirmed: `bundle/vuln_variant/reproduction_steps.sh` was run twice. Both runs completed without crashing and exited `1`, which is the expected script result for “alternate trigger on vulnerable version only; no fixed-version bypass.”
- The prepared repository worktree itself was not checked out or mutated in place for testing; isolated worktrees under `bundle/vuln_variant/build_request_variant/worktrees/` were used.
- The repository lacked an explicit `SECURITY.md`; no target policy was found that excluded proxied HTTP message parsing from security scope.
