PYSEC-2026-3572
Vulnerability from pysec - Published: 2026-08-04 11:34 - Updated: 2026-08-04 13:36Summary
Numerous HTTP-emitting modules (core.api.http_get, core.api.http_post, graphql.query/graphql.mutation, monitor.http_check, communication.slack_send, notification.{discord,slack,teams}.send_message, ai.vision_analyze [anthropic path], verify.visual_diff, browser.proxy_rotate, and the agent/llm inline base_url branch) perform outbound requests to a fully client-controlled URL without calling the project's own SSRF guard (validate_url_with_env_config) that their sibling modules apply. An authenticated workflow-author can point the URL at the cloud metadata IP (169.254.169.254), a loopback/RFC1918 host, or any internal host and read the response, yielding cloud-metadata credential theft and internal service read/write.
Root Cause
The SSRF guard is per-module (there is NO global egress interception). Each module must call validate_url_with_env_config before issuing a request. The listed modules never call it — they only carry an ssrf_protected metadata tag string which enforces nothing. Exemplar: src/core/modules/third_party/developer/http/requests.py — grepping for validate_url|ssrf|is_private in requests.py returns 0 guard calls; session.get(url) fires at :85 (HTTPGetModule) and session.post at :188 (HTTPPostModule). SECURITY.md incorrectly lists api.http_get as SSRF-protected.
Impact
Readable SSRF: full {status_code, headers, body} returned to the caller (requests.py:96-108). Enables theft of cloud IAM credentials from the metadata endpoint and read/write access to internal-only APIs. Scope Changed (S:C) — the request crosses into cloud-metadata / internal-network authority the workflow layer does not otherwise have.
Proof of Concept
Verified live this session: core.api.http_get with url pointed at a loopback internal server returned the internal body INTERNAL-SECRET-IAM-CREDENTIALS, while the guarded sibling http.get returned NETWORK_ERROR: Hostname blocked: 127.0.0.1 on the same input — proving the branch-asymmetry is real (not a port artifact).
POST /mcp {"method":"tools/call","params":{"name":"execute_module",
"arguments":{"module_id":"core.api.http_get",
"params":{"url":"http://<cloud-metadata-ip>/latest/meta-data/iam/security-credentials/"}}}}
Attack Chain
- Entry: authenticated MCP client →
POST /mcpexecute_modulecore.api.http_get, url set to the cloud metadata endpoint. Guard:require_auth(mcp.py:71). Bypass proof: passes with a valid workflow-author bearer token (PR:L). - Check: capability denylist /
enforce_module_policy(base.py:240). Bypass proof:core.api.*not in_DEFAULT_DENYLIST(module_policy.py:45-67) →is_allowed=True(runtime-registration verified:core.api.http_get -> HTTPGetModule). - Check: SSRF validation. Bypass proof:
requests.pyhas ZEROvalidate_url/ssrf calls — only thessrf_protectedtag string at :23/:116.session.get(url)fires at :85. - Sink: aiohttp GET/POST to the cloud metadata IP.
- Impact: full response body returned → IAM credential theft, internal read/write.
Bypass Evidence
Grepping validate_url|ssrf|is_private in requests.py → 0 guard calls (2 hits are both the inert tag string). Live PoC returned internal body directly; guarded sibling blocked the same input. Direct IP works — no IPv6 transition trick needed (AC:L), unlike the seed CVE-2026-55787.
Affected Versions
<= 2.26.6 — code present on latest release tag v2.26.6 (requests.py:19,112).
Suggested Fix
Call validate_url_with_env_config(url) in each listed module before issuing the outbound request, matching the guarded siblings (e.g. ai.model:157, http.get). Best: route all outbound HTTP through a single guarded client wrapper so new modules inherit the guard.
Credit
Vulnerability discovered by zx (Jace).
| Name | purl | flyto-core | pkg:pypi/flyto-core |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "flyto-core",
"purl": "pkg:pypi/flyto-core"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.26.7"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"1.0.0",
"1.0.1",
"1.0.2",
"1.0.3",
"1.0.4",
"1.0.5",
"1.0.6",
"1.0.7",
"1.0.8",
"1.0.9",
"1.1.0",
"1.1.1",
"1.11.0",
"1.12.0",
"1.13.0",
"1.14.0",
"1.14.1",
"1.14.2",
"1.15.0",
"1.16.0",
"1.16.1",
"1.16.10",
"1.16.2",
"1.16.3",
"1.16.4",
"1.16.5",
"1.16.6",
"1.16.7",
"1.16.8",
"1.16.9",
"1.2.0",
"1.3.0",
"1.4.0",
"1.5.0",
"1.5.1",
"1.5.2",
"1.5.4",
"1.6.0",
"1.6.1",
"1.6.2",
"1.6.3",
"1.6.4",
"1.6.5",
"1.7.0",
"1.7.1",
"1.7.2",
"1.7.3",
"1.7.4",
"1.7.5",
"1.7.6",
"1.7.7",
"1.7.8",
"1.7.9",
"1.8.0",
"1.8.1",
"1.8.10",
"1.8.11",
"1.8.12",
"1.8.13",
"1.8.14",
"1.8.15",
"1.8.16",
"1.8.17",
"1.8.2",
"1.8.3",
"1.8.4",
"1.8.5",
"1.8.6",
"1.8.7",
"1.8.8",
"1.8.9",
"1.9.0",
"2.0.0",
"2.0.1",
"2.0.2",
"2.0.3",
"2.0.4",
"2.0.5",
"2.1.0",
"2.1.1",
"2.1.2",
"2.1.3",
"2.1.4",
"2.10.0",
"2.11.0",
"2.12.0",
"2.12.1",
"2.12.13",
"2.12.15",
"2.12.16",
"2.12.17",
"2.12.18",
"2.12.19",
"2.12.2",
"2.12.20",
"2.12.21",
"2.12.22",
"2.12.23",
"2.12.24",
"2.12.25",
"2.12.26",
"2.12.27",
"2.12.28",
"2.12.3",
"2.12.4",
"2.12.5",
"2.12.6",
"2.13.0",
"2.13.1",
"2.13.2",
"2.13.3",
"2.13.4",
"2.14.0",
"2.15.0",
"2.15.1",
"2.15.2",
"2.15.3",
"2.16.1",
"2.16.3",
"2.16.4",
"2.17.0",
"2.17.1",
"2.17.2",
"2.17.3",
"2.17.4",
"2.17.5",
"2.17.6",
"2.17.7",
"2.17.8",
"2.18.0",
"2.18.1",
"2.18.10",
"2.18.11",
"2.18.2",
"2.18.3",
"2.18.4",
"2.18.5",
"2.18.6",
"2.18.8",
"2.18.9",
"2.19.0",
"2.2.0",
"2.2.1",
"2.2.2",
"2.20.0",
"2.20.1",
"2.20.2",
"2.20.3",
"2.20.4",
"2.23.0",
"2.23.1",
"2.23.2",
"2.23.3",
"2.24.0",
"2.24.1",
"2.24.2",
"2.24.3",
"2.24.4",
"2.25.0",
"2.25.1",
"2.25.10",
"2.25.11",
"2.25.12",
"2.25.13",
"2.25.14",
"2.25.15",
"2.25.16",
"2.25.17",
"2.25.18",
"2.25.19",
"2.25.2",
"2.25.20",
"2.25.21",
"2.25.22",
"2.25.23",
"2.25.24",
"2.25.25",
"2.25.26",
"2.25.27",
"2.25.3",
"2.25.4",
"2.25.5",
"2.25.6",
"2.25.7",
"2.25.8",
"2.25.9",
"2.26.0",
"2.26.1",
"2.26.2",
"2.26.3",
"2.26.4",
"2.26.5",
"2.3.0",
"2.3.1",
"2.4.0",
"2.4.1",
"2.4.2",
"2.4.3",
"2.4.4",
"2.4.5",
"2.4.6",
"2.4.7",
"2.5.0",
"2.5.1",
"2.5.2",
"2.6.0",
"2.6.1",
"2.7.0",
"2.7.1",
"2.7.2",
"2.7.3",
"2.7.4",
"2.7.5",
"2.7.6",
"2.8.0",
"2.9.0"
]
}
],
"aliases": [
"CVE-2026-67428",
"GHSA-pgwh-4jj4-qm8v"
],
"details": "## Summary\nNumerous HTTP-emitting modules (`core.api.http_get`, `core.api.http_post`, `graphql.query`/`graphql.mutation`, `monitor.http_check`, `communication.slack_send`, `notification.{discord,slack,teams}.send_message`, `ai.vision_analyze` [anthropic path], `verify.visual_diff`, `browser.proxy_rotate`, and the `agent`/`llm` inline base_url branch) perform outbound requests to a fully client-controlled URL **without calling the project\u0027s own SSRF guard** (`validate_url_with_env_config`) that their sibling modules apply. An authenticated workflow-author can point the URL at the cloud metadata IP (169.254.169.254), a loopback/RFC1918 host, or any internal host and read the response, yielding cloud-metadata credential theft and internal service read/write.\n\n## Root Cause\nThe SSRF guard is per-module (there is NO global egress interception). Each module must call `validate_url_with_env_config` before issuing a request. The listed modules never call it \u2014 they only carry an `ssrf_protected` **metadata tag string** which enforces nothing. Exemplar: `src/core/modules/third_party/developer/http/requests.py` \u2014 grepping for `validate_url|ssrf|is_private` in requests.py returns 0 guard calls; `session.get(url)` fires at `:85` (HTTPGetModule) and `session.post` at `:188` (HTTPPostModule). SECURITY.md incorrectly lists `api.http_get` as SSRF-protected.\n\n## Impact\nReadable SSRF: full `{status_code, headers, body}` returned to the caller (requests.py:96-108). Enables theft of cloud IAM credentials from the metadata endpoint and read/write access to internal-only APIs. Scope Changed (S:C) \u2014 the request crosses into cloud-metadata / internal-network authority the workflow layer does not otherwise have.\n\n## Proof of Concept\nVerified live this session: `core.api.http_get` with `url` pointed at a loopback internal server returned the internal body `INTERNAL-SECRET-IAM-CREDENTIALS`, while the guarded sibling `http.get` returned `NETWORK_ERROR: Hostname blocked: 127.0.0.1` on the same input \u2014 proving the branch-asymmetry is real (not a port artifact).\n```\nPOST /mcp {\"method\":\"tools/call\",\"params\":{\"name\":\"execute_module\",\n \"arguments\":{\"module_id\":\"core.api.http_get\",\n \"params\":{\"url\":\"http://\u003ccloud-metadata-ip\u003e/latest/meta-data/iam/security-credentials/\"}}}}\n```\n\n## Attack Chain\n1. Entry: authenticated MCP client \u2192 `POST /mcp` execute_module `core.api.http_get`, url set to the cloud metadata endpoint. Guard: `require_auth` (mcp.py:71). Bypass proof: passes with a valid workflow-author bearer token (PR:L).\n2. Check: capability denylist / `enforce_module_policy` (base.py:240). Bypass proof: `core.api.*` not in `_DEFAULT_DENYLIST` (module_policy.py:45-67) \u2192 `is_allowed=True` (runtime-registration verified: `core.api.http_get -\u003e HTTPGetModule`).\n3. Check: SSRF validation. Bypass proof: `requests.py` has ZERO `validate_url`/ssrf calls \u2014 only the `ssrf_protected` tag string at :23/:116. `session.get(url)` fires at :85.\n4. Sink: aiohttp GET/POST to the cloud metadata IP.\n5. Impact: full response body returned \u2192 IAM credential theft, internal read/write.\n\n## Bypass Evidence\nGrepping `validate_url|ssrf|is_private` in requests.py \u2192 0 guard calls (2 hits are both the inert tag string). Live PoC returned internal body directly; guarded sibling blocked the same input. Direct IP works \u2014 no IPv6 transition trick needed (AC:L), unlike the seed CVE-2026-55787.\n\n## Affected Versions\n`\u003c= 2.26.6` \u2014 code present on latest release tag v2.26.6 (`requests.py:19,112`).\n\n## Suggested Fix\nCall `validate_url_with_env_config(url)` in each listed module before issuing the outbound request, matching the guarded siblings (e.g. `ai.model:157`, `http.get`). Best: route all outbound HTTP through a single guarded client wrapper so new modules inherit the guard.\n\n## Credit\n\nVulnerability discovered by zx (Jace).",
"id": "PYSEC-2026-3572",
"modified": "2026-08-04T13:36:18.753670Z",
"published": "2026-08-04T11:34:45.633302Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/flytohub/flyto-core/security/advisories/GHSA-pgwh-4jj4-qm8v"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-67428"
},
{
"type": "WEB",
"url": "https://github.com/flytohub/flyto-core/commit/0a0a528520ec18f5a21f1ddf858a71cc1edfb6e9"
},
{
"type": "PACKAGE",
"url": "https://github.com/flytohub/flyto-core"
},
{
"type": "WEB",
"url": "https://github.com/flytohub/flyto-core/releases/tag/v2.26.7"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/flyto-core"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-pgwh-4jj4-qm8v"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Flyto2 Core: Multiple HTTP-family modules fetch client-controlled URLs without the SSRF guard their siblings apply (SSRF to internal/metadata)"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.