First of the prerequisites for hosting this as a service. It is also a fix worth having on its own.
The problem
zerto_recover_file took a dest_dir parameter, built a path from it with no containment check, created that directory, and wrote the recovered bytes there:
dest=Path(dest_diror_settings.get("recovery_dir")or"./recovered")# caller-supplieddest.mkdir(parents=True,exist_ok=True)started=awaitclient.start_flr(...,str(dest.resolve()))# ← sent to the ZVMout_path.write_bytes(blob)
On one developer's laptop that reads as "write to my own disk". Shared, it is an arbitrary write. And the same string goes to the appliance as initialDownloadPath, so the caller also dictates where the ZVM mounts — a UNC path from a domain-joined appliance is an outbound authentication attempt, whatever 10.9 does with it today.
The parameter is gone. The destination comes from config only.
Returning a path was also just wrong
A path on this host means nothing to a caller on another machine. Hosted, the recover half of the loop would have quietly stopped being useful the first time someone used it — the tool would report success and the developer would have no file.
The tool now returns the content: text where it decodes as UTF-8, base64 otherwise, plus bytes and a sha256 so the caller can verify what they got.
Files over max_recover_bytes (1 MiB default) are refused and pointed at the whole-VM ladder rather than streamed through a tool result. FLR is for a config file or a dropped directory, not a disk image.
Verified live against ZVM 10.9.10
advertised params: [checkpoint_identifier, confirmed, guest_path, vm_identifier, vpg_identifier]
linux jp-ubuntu: ok=True bytes=158 encoding=text path_in_response=False
windows ad1 : ok=True bytes=164 encoding=text path_in_response=False
Both returned correct content, both sessions unmounted, and dest_dir no longer appears in the advertised schema.
pytest: 51 passed (3 new). One of them asserts the exact parameter set, so a caller-supplied destination cannot creep back in unnoticed.
Note for whoever merges
This breaks the demo harness in #8, which reads rec["path"] in six places (demo/driver.py:215-229, demo/windows_driver.py:158,169). Those need to switch to rec["content"]. Whichever of #8 or this merges second should carry the fix.
First of the prerequisites for hosting this as a service. It is also a fix worth having on its own.
## The problem
`zerto_recover_file` took a `dest_dir` parameter, built a path from it with **no containment check**, created that directory, and wrote the recovered bytes there:
```python
dest = Path(dest_dir or _settings.get("recovery_dir") or "./recovered") # caller-supplied
dest.mkdir(parents=True, exist_ok=True)
started = await client.start_flr(..., str(dest.resolve())) # ← sent to the ZVM
out_path.write_bytes(blob)
```
On one developer's laptop that reads as "write to my own disk". Shared, it is an arbitrary write. And the same string goes to the appliance as `initialDownloadPath`, so the caller also dictates where the **ZVM** mounts — a UNC path from a domain-joined appliance is an outbound authentication attempt, whatever 10.9 does with it today.
The parameter is gone. The destination comes from config only.
## Returning a path was also just wrong
A path on this host means nothing to a caller on another machine. Hosted, the recover half of the loop would have quietly stopped being useful the first time someone used it — the tool would report success and the developer would have no file.
The tool now returns the **content**: `text` where it decodes as UTF-8, `base64` otherwise, plus `bytes` and a `sha256` so the caller can verify what they got.
Files over `max_recover_bytes` (1 MiB default) are refused and pointed at the whole-VM ladder rather than streamed through a tool result. FLR is for a config file or a dropped directory, not a disk image.
## Verified live against ZVM 10.9.10
```
advertised params: [checkpoint_identifier, confirmed, guest_path, vm_identifier, vpg_identifier]
linux jp-ubuntu: ok=True bytes=158 encoding=text path_in_response=False
windows ad1 : ok=True bytes=164 encoding=text path_in_response=False
```
Both returned correct content, both sessions unmounted, and `dest_dir` no longer appears in the advertised schema.
`pytest`: 51 passed (3 new). One of them asserts the exact parameter set, so a caller-supplied destination cannot creep back in unnoticed.
## Note for whoever merges
This **breaks the demo harness in #8**, which reads `rec["path"]` in six places (`demo/driver.py:215-229`, `demo/windows_driver.py:158,169`). Those need to switch to `rec["content"]`. Whichever of #8 or this merges second should carry the fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
zerto_recover_file took a dest_dir parameter, built a path from it with no
containment check, created that directory, and wrote the recovered bytes
there. It also passed the same string to the appliance as
initialDownloadPath.
On one developer's laptop that reads as "write to my own disk". The moment
this server is shared it is an arbitrary write, and a caller-supplied path
handed to another system is worse than it looks: a UNC path from a
domain-joined appliance is an outbound authentication attempt.
The parameter is gone. The destination now comes from config only, so the
caller can influence neither where bytes land nor where the ZVM mounts.
Returning a path was also useless to a remote caller. A path on this host
means nothing on theirs, so the whole recover half of the loop would have
quietly stopped working the first time this was hosted. The tool now
returns the content: text where it decodes as UTF-8, base64 otherwise,
with the byte count and a sha256 so the caller can verify what they got.
Files over max_recover_bytes (1 MiB default) are refused and pointed at
the whole-VM ladder instead of streamed through a tool result. FLR is for
a config file or a dropped directory.
Verified live against ZVM 10.9.10: jp-ubuntu returned 158 bytes and ad1
returned 164, both as text with matching content, both sessions
unmounted, and dest_dir no longer appears in the advertised tool schema.
pytest 51 passed (3 new, including one asserting the parameter set so the
caller-supplied destination cannot come back by accident).
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
First of the prerequisites for hosting this as a service. It is also a fix worth having on its own.
The problem
zerto_recover_filetook adest_dirparameter, built a path from it with no containment check, created that directory, and wrote the recovered bytes there:On one developer's laptop that reads as "write to my own disk". Shared, it is an arbitrary write. And the same string goes to the appliance as
initialDownloadPath, so the caller also dictates where the ZVM mounts — a UNC path from a domain-joined appliance is an outbound authentication attempt, whatever 10.9 does with it today.The parameter is gone. The destination comes from config only.
Returning a path was also just wrong
A path on this host means nothing to a caller on another machine. Hosted, the recover half of the loop would have quietly stopped being useful the first time someone used it — the tool would report success and the developer would have no file.
The tool now returns the content:
textwhere it decodes as UTF-8,base64otherwise, plusbytesand asha256so the caller can verify what they got.Files over
max_recover_bytes(1 MiB default) are refused and pointed at the whole-VM ladder rather than streamed through a tool result. FLR is for a config file or a dropped directory, not a disk image.Verified live against ZVM 10.9.10
Both returned correct content, both sessions unmounted, and
dest_dirno longer appears in the advertised schema.pytest: 51 passed (3 new). One of them asserts the exact parameter set, so a caller-supplied destination cannot creep back in unnoticed.Note for whoever merges
This breaks the demo harness in #8, which reads
rec["path"]in six places (demo/driver.py:215-229,demo/windows_driver.py:158,169). Those need to switch torec["content"]. Whichever of #8 or this merges second should carry the fix.🤖 Generated with Claude Code
https://claude.ai/code/session_016yVfC5nvZowoLFnEGWhLGn
b4fcb59094to27799f1154