fix(workflows): validate every redirect hop when fetching workflow/step catalogs (#3637)

WorkflowCatalog._fetch_single_catalog and StepCatalog._fetch_single_catalog
opened the catalog URL with open_url(entry.url, timeout=30) and validated
only the final resp.geturl(). open_url follows redirects, so an https://
catalog entry that 30x-redirects through a non-HTTPS host mid-chain could
let a network attacker rewrite the next hop and slip a payload past the
terminal-URL-only check. The payload then drives step/workflow catalog data.

Pass a redirect_validator that runs the existing HTTPS/hostname check before
every redirect hop, keeping the final geturl() check as a defense-in-depth
backstop. This brings both workflow catalog loaders to parity with the
presets (#3523) and extensions (#3524) catalog fetchers.

Tests: add per-hop redirect-validation tests for both WorkflowCatalog and
StepCatalog (a non-HTTPS intermediate hop is rejected); both fail before the
fix ("NoneType object is not callable" — no validator passed). Update the two
existing malformed-redirect tests whose open_url stub lacked the
redirect_validator kwarg.
This commit is contained in:
Quratulain-bilal
2026-07-22 15:58:38 +05:00
committed by GitHub
parent 717d7c4b89
commit 30e99ec083
2 changed files with 102 additions and 4 deletions

View File

@@ -523,8 +523,20 @@ class WorkflowCatalog:
_validate_catalog_url(entry.url)
# Validate EVERY redirect hop, not just the final URL: _open_url follows
# redirects, so an https:// entry that 30x-redirects through http:// (or
# to a non-HTTPS host mid-chain) could otherwise let a network attacker
# rewrite the next hop and slip a payload past a final-URL-only check.
# redirect_validator runs before each hop; the geturl() check below is
# retained as a defense-in-depth backstop. Mirrors the presets/extensions
# catalog fix (#3523 / #3524).
def _validate_redirect(_old_url: str, new_url: str) -> None:
_validate_catalog_url(new_url)
try:
with _open_url(entry.url, timeout=30) as resp:
with _open_url(
entry.url, timeout=30, redirect_validator=_validate_redirect
) as resp:
_validate_catalog_url(resp.geturl())
data = json.loads(resp.read().decode("utf-8"))
except Exception as exc:
@@ -1180,8 +1192,20 @@ class StepCatalog:
_validate_url(entry.url)
# Validate EVERY redirect hop, not just the final URL: _open_url follows
# redirects, so an https:// entry that 30x-redirects through http:// (or
# to a non-HTTPS host mid-chain) could otherwise let a network attacker
# rewrite the next hop and slip a payload past a final-URL-only check.
# redirect_validator runs before each hop; the geturl() check below is
# retained as a defense-in-depth backstop. Mirrors the presets/extensions
# catalog fix (#3523 / #3524).
def _validate_redirect(_old_url: str, new_url: str) -> None:
_validate_url(new_url)
try:
with _open_url(entry.url, timeout=30) as resp:
with _open_url(
entry.url, timeout=30, redirect_validator=_validate_redirect
) as resp:
_validate_url(resp.geturl())
data = json.loads(resp.read().decode("utf-8"))
except Exception as exc: