Title: xAI image generation provider performs an unguarded host-side fetch of provider-controlled image URLs
Description:
Hermes' image_generate tool allows the configured xAI image-generation provider to return a data[0].url value that Hermes then downloads from the host with a raw requests.get(). Because this second-hop fetch does not use the repository's existing URL safety checks or redirect-target revalidation, a malicious or compromised provider, relay, or upstream response path can cause the Hermes host to request attacker-chosen URLs. In practical deployments this can expose loopback services, internal HTTP endpoints, or cloud metadata targets reachable from the Hermes process.
The public entrypoint is the image_generate tool in tools/image_generation_tool.py. When image_gen.provider is configured to xai, _handle_image_generate() dispatches the request into the plugin provider path:
def _handle_image_generate(args, **kw):
prompt = args.get("prompt", "")
...
dispatched = _dispatch_to_plugin_provider(prompt, aspect_ratio)At the xAI provider layer, the response parser accepts either inline base64 image data or a remote image URL. If data[0].url is present, the provider hands that URL to save_url_image():
first = data[0]
b64 = first.get("b64_json")
url = first.get("url")
...
elif url:
saved_path = save_url_image(url, prefix=f"xai_{model_id}")The sink is in agent/image_gen_provider.py. save_url_image() performs a direct network fetch from the Hermes host:
response = requests.get(url, timeout=timeout, stream=True)
response.raise_for_status()There is no call to tools.url_safety.is_safe_url, no equivalent async guard, and no redirect-target revalidation before dereferencing the provider-controlled URL. This creates an SSRF-style trust boundary failure on the provider response path rather than on a user-supplied URL field.
I verified the issue through the real Hermes tool interface, not a unit-test-only harness. The exploit path was:
model_tools.handle_function_call("image_generate", ...) -> configured xai provider -> provider response data[0].url -> save_url_image(url) -> requests.get(url)
The attached integration PoC uses a localhost xAI-compatible stub service. In the control case the stub returns b64_json and Hermes does not perform a second-hop fetch. In the vulnerable case the stub returns data[0].url pointing at /canary.png, and Hermes performs the fetch from the host. The observation logs show:
- Control:
provider_hits=1,canary_hits=0 - Vulnerable case:
provider_hits=1,canary_hits=1
- A local checkout of
NousResearch/hermes-agent - Python environment able to import Hermes from the repository root
- No special credentials beyond setting
XAI_API_KEYto any non-empty value for provider availability checks - The PoC scripts below downloaded from the linked secret gists
-
Download the provider stub from: stub_xai_server.py
-
Download the vulnerable-path reproducer from: verification_test.py
-
Download the control script from: control-normal-b64.py
-
Place the three scripts in the same directory and run the control case first from the repository root:
python3 llm-enhance/cve-finding/similar/Injection/Advisory-GHSA-qxgf-hmcj-3xw3-image-gen-provider-save-url-image-exp/control-normal-b64.py -
Confirm the control case prints:
[CONTROL-PROTECTED] -
Run the vulnerable case:
python3 llm-enhance/cve-finding/similar/Injection/Advisory-GHSA-qxgf-hmcj-3xw3-image-gen-provider-save-url-image-exp/verification_test.py -
Confirm the vulnerable case prints:
[DEFECT-CONFIRMED-WITH-LIMITATIONS] -
Inspect the generated logs:
control_observation.logshould containprovider_hits=1andcanary_hits=0verification_observation.logshould containprovider_hits=1andcanary_hits=1
Control run:
mode=control-b64
provider_hits=1
canary_hits=0
provider_requests=[{"model": "grok-imagine-image", "prompt": "ssrf-control-b64", "aspect_ratio": "1:1", "resolution": "1k"}]
rc=0
Vulnerable run:
mode=vuln-url
provider_hits=1
canary_hits=1
provider_requests=[{"model": "grok-imagine-image", "prompt": "ssrf-vuln-url", "aspect_ratio": "1:1", "resolution": "1k"}]
rc=0
This is an SSRF-style host-side fetch issue on the image provider response path. Any Hermes deployment that enables the xai image-generation provider is affected if the provider endpoint, a relay in front of it, or an otherwise trusted upstream can be induced to return attacker-chosen image URLs. The impacted asset is the Hermes host's network position. An attacker can potentially use Hermes to request loopback services, internal HTTP endpoints, cloud metadata resources, or other network targets reachable from the process but not directly reachable from the attacker.
- Ecosystem: pip
- Package name: hermes-agent
- Affected versions: <= 0.16.0
- Patched versions:
- Severity: Medium
- Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
- CWE: CWE-918: Server-Side Request Forgery (SSRF)
| Permalink | Description |
|---|---|
| https://github.com/NousResearch/hermes-agent/blob/3c231eb3979ab9c57d5cd6d02f1d577a3b718b43/tools/image_generation_tool.py#L1033-L1043 | Public image_generate handler that dispatches the user request into the configured plugin provider path. |
| https://github.com/NousResearch/hermes-agent/blob/3c231eb3979ab9c57d5cd6d02f1d577a3b718b43/plugins/image_gen/xai/init.py#L267-L294 | xAI provider response parser that accepts data[0].url and forwards it to save_url_image(). |
| https://github.com/NousResearch/hermes-agent/blob/3c231eb3979ab9c57d5cd6d02f1d577a3b718b43/agent/image_gen_provider.py#L207-L229 | Vulnerable downloader that performs a raw requests.get(url, timeout=timeout, stream=True) on the provider-controlled URL without SSRF validation or redirect-target revalidation. |