---
name: report-egress-bypass
description: Write the PoC and report for a host that refuses direct requests but serves through an egress route such as CloudFront or Azure Functions (`toadcli egress` MORE ACCESS). Use when validating or reporting a finding whose access depends on a route, not for a host that only refuses our address (`refused here`), which a triager reaches directly.
---

# Report an egress route bypass

The bug is the host's allowlist, not our route: it trusts a shared provider's addresses, which every customer of that provider sends from, rather than only its own front end or servers. The report shows that anyone can do it, and what the allowlist protects.

- The description names the host, how it refuses direct requests (the status, or a dropped connection), and that it serves the same URL to anyone on the provider, because it allowlists the provider's shared addresses. It gives the provider's fix below.
- The impact is what sits behind the allowlist (an admin panel, an internal API, data), shown as `write-finding-report` describes. A 2xx page through the route is not the impact by itself.
- The direct refusal is this bug's baseline, not a negative control: the PoC and the manual steps show it.

Each route has a PoC template in this skill's `scripts/`. Copy it to `work/scratch/<finding-id>/<name>.py` as the one attachment, and set its docstring, `TARGET`, `HEADERS` and `proof()`. Leave everything from `# Your own ...: setup and delete.` down as it is: it sends the triager's cloud credentials, so it keeps TLS verification. With no command the PoC requests TARGET directly and then through our route, and exits non-zero unless the direct request is refused and the route returns the proof. Its `setup` subcommand creates the route in the triager's own cloud account and prints the URL to pass instead of ours; `delete` removes it. The automated reproduction gives `uv run <name>.py` with its output, then the own-account route below.

`toadcli egress-poc ROUTE HOST` prints our route's URL for the PoC: it reaches only HOST, so it is safe to hand out. The route's bare URL and `managed_function_secret` never go in a report.

## CloudFront

The template is `scripts/cloudfront-poc.py`. Also set `ORIGIN`, and `CLOUDFRONT_URL` to what `toadcli egress-poc cloudfront ORIGIN` prints. The fix is for the origin to accept only its own distribution, for example by checking a secret custom header that distribution adds, or by using CloudFront VPC origins.

```bash
uv run <name>.py setup --access-key-id AKIA... --secret-access-key ...
uv run <name>.py --cloudfront-url <the URL setup printed>
uv run <name>.py delete --access-key-id AKIA... --secret-access-key ...
```

Say that setup takes about five minutes and that the key needs CloudFrontFullAccess or the inline policy `uv run <name>.py setup -h` prints.

The manual steps are the direct request and then the same request to our CloudFront URL with the `x-toadex-origin`, `x-toadex-host`, `x-toadex-proto` and `x-toadex-port` headers the PoC sends, each with its response.

## Azure Functions

The template is `scripts/azure-functions-poc.py`. Set `FUNCTION_URL` to what `toadcli egress-poc azure_functions TARGET` prints. The function sends the request from Azure's shared outbound addresses. The fix is for the host to stop allowlisting Azure address ranges (such as the AzureCloud service tag), and to allow only its own fixed addresses, such as a NAT gateway's, or a private endpoint.

```bash
az login
uv run <name>.py setup
uv run <name>.py --function-url <the URL setup printed>
uv run <name>.py delete
```

Say that setup takes a few minutes, that it needs the Contributor role on the subscription, and that a service principal works instead of `az login` (`uv run <name>.py setup -h`).

The manual steps are the direct request and then the POST to our function URL that the PoC sends, with its JSON body, each with its response: the function returns the target's status, headers and base64 body as JSON.
