Skip to content

Add a central SSRF egress guard for outbound action requests - #763

Open
Karal wants to merge 1 commit into
looker-open-source:masterfrom
Karal:fix/ssrf-central-egress-guard
Open

Karal wants to merge 1 commit into
looker-open-source:masterfrom
Karal:fix/ssrf-central-egress-guard

Conversation

@Karal

@Karal Karal commented Aug 29, 2026

Copy link
Copy Markdown

Summary

Action Hub makes outbound HTTP requests to hosts that are supplied, in whole or in part, by the inbound action request. Several of these paths perform no validation of the destination, so a request can be pointed at loopback, link-local (including the cloud metadata endpoint at 169.254.169.254), or private address space — a Server-Side Request Forgery (SSRF) primitive.

This change introduces a single egress guard in src/hub and applies it to the streaming subsystem and to the per-action destinations that lacked (or had bypassable) validation.

Details

New module src/hub/ssrf_filter.ts:

  • isRestrictedAddress(ip) — classifies an address literal against loopback, link-local, private (RFC 1918), CGNAT, and other reserved IPv4/IPv6 ranges, including IPv4-mapped IPv6.
  • ssrfSafeLookup(hostname, options, cb) — a dns.lookup-compatible function that errors if the host resolves to a restricted address. Passed as the lookup option of an outbound request, it validates the address the socket actually connects to, which also closes DNS-rebinding races.
  • assertPublicUrl(url) — validates the protocol and resolved host ahead of a request, for callers that use a client library which cannot accept a custom lookup.

Call sites:

  • ActionRequest.stream() (src/hub/action_request.ts) — scheduled_plan.download_url is now fetched with ssrfSafeLookup. request does not re-apply the lookup option to redirect targets, so redirects are followed manually and every hop is validated; this closes the 302 -> http://<internal>/ bypass.
  • jira, marketo, salesforce — validate the request-supplied server address / instance URL with assertPublicUrl before constructing the client.
  • digitalocean, azure — the region / account name is interpolated into the destination host, so it is restricted to the provider's identifier charset to prevent host injection.
  • braze — the endpoint was validated with a substring match (^https?://(.*)\.braze\.(com|eu)$), which https://169.254.169.254/x.braze.com passes while the real host is internal. It is now validated against the parsed URL hostname.

Testing

  • test/test_ssrf_filter.ts — unit coverage for the address classifier, assertPublicUrl, and ssrfSafeLookup.
  • test/test_action_request.tsstream() refuses a download url that targets an internal address, and refuses one that redirects toward an internal address.
  • test/test_braze.ts — regression test for the endpoint-host bypass.
  • yarn test (mocha + tsc --noEmit + tslint) passes.

Note for self-hosted deployments

The guard blocks private and reserved address space by default. A deployment that runs the Action Hub and its Looker instance entirely on an internal network may need the streaming download URL to reach a private address; such deployments would need an allowance mechanism. This change keeps the secure default and does not add configuration, matching the reported issue; an opt-in allowlist can be layered on if maintainers want to support that topology.

The streaming subsystem (ActionRequest.stream) fetched
scheduled_plan.download_url with no validation and followed redirects,
so the response of an internal service could be streamed out to an
action destination. Several actions also passed a request-supplied host
straight to their client without validation (jira, marketo, salesforce),
interpolated a request-supplied value into the destination host
(digitalocean, azure), or validated the endpoint with a substring match
that the real host could bypass (braze).

Add src/hub/ssrf_filter.ts, which rejects hosts that resolve to
loopback, link-local (including the cloud metadata endpoint), private,
and other reserved address ranges. The streaming path validates every
redirect hop with a custom lookup and an up-front literal check, and the
affected actions validate their destination before making a request.
@Karal
Karal requested a review from a team as a code owner August 29, 2026 11:20
@google-cla

google-cla Bot commented Aug 29, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant