Skip to content

fix: Harden OTEL endpoint validation#12954

Merged
anthonyshew merged 3 commits into
mainfrom
shew/fix-otel-endpoint-validation
May 26, 2026
Merged

fix: Harden OTEL endpoint validation#12954
anthonyshew merged 3 commits into
mainfrom
shew/fix-otel-endpoint-validation

Conversation

@anthonyshew

@anthonyshew anthonyshew commented May 26, 2026

Copy link
Copy Markdown
Contributor

Why

Experimental OTEL endpoints are user-configurable and should not allow direct targeting of internal network or cloud metadata endpoints. The existing validation already rejected insecure HTTP URLs, but HTTPS literal IPs and metadata hostnames still needed defense-in-depth coverage.

Fixes #12941.

What

Reject OTEL endpoints that use unsafe literal IP ranges or known metadata-service hostnames while preserving supported local collector workflows through the localhost hostname. Update the OTEL example and architecture docs to reflect the endpoint rules.

How

Added endpoint host validation for private, loopback, link-local, multicast, documentation, carrier-grade NAT, and metadata-service IPs, plus metadata hostnames. Added focused unit coverage and verified existing OTEL integration behavior.

@anthonyshew anthonyshew requested a review from a team as a code owner May 26, 2026 21:40
@vercel

vercel Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
examples-basic-web Ready Ready Preview, Comment, Open in v0 May 26, 2026 10:05pm
examples-designsystem-docs Ready Ready Preview, Comment, Open in v0 May 26, 2026 10:05pm
examples-gatsby-web Ready Ready Preview, Comment, Open in v0 May 26, 2026 10:05pm
examples-kitchensink-blog Ready Ready Preview, Comment, Open in v0 May 26, 2026 10:05pm
examples-nonmonorepo Ready Ready Preview, Comment, Open in v0 May 26, 2026 10:05pm
examples-svelte-web Ready Ready Preview, Comment, Open in v0 May 26, 2026 10:05pm
examples-tailwind-web Ready Ready Preview, Comment, Open in v0 May 26, 2026 10:05pm
examples-vite-web Ready Ready Preview, Comment, Open in v0 May 26, 2026 10:05pm
turbo-site Ready Ready Preview, Comment, Open in v0 May 26, 2026 10:05pm

@anthonyshew anthonyshew requested review from tknickman and removed request for a team May 26, 2026 21:40
Comment thread crates/turborepo-run-summary/src/observability/otel.rs
vercel Bot and others added 2 commits May 26, 2026 21:50
…fff:127.0.0.1`) bypass the SSRF protection in `is_blocked_ipv6`, allowing access to cloud metadata endpoints and internal services.

This commit fixes the issue reported at crates/turborepo-run-summary/src/observability/otel.rs:171

## Bug Analysis

The `is_blocked_ipv6` function is designed to prevent SSRF attacks by blocking requests to internal/private IPv6 addresses. However, it does not account for IPv4-mapped IPv6 addresses (the `::ffff:0:0/96` prefix range).

**How the bypass works:**

1. An attacker configures the OTel endpoint to `https://[::ffff:169.254.169.254]/latest/meta-data/`
2. The `url` crate (v2.5.7, following WHATWG spec) parses this as `Host::Ipv6(Ipv6Addr)` with segments `[0, 0, 0, 0, 0, 0xffff, 0xa9fe, 0xa9fe]`
3. `is_blocked_otel_host` dispatches to `is_blocked_ipv6`
4. In `is_blocked_ipv6`, none of the existing checks match:
   - `is_loopback()` → false (only matches `::1`)
   - `is_unspecified()` → false (only matches `::`)
   - ULA check `(first & 0xfe00) == 0xfc00` → `(0 & 0xfe00) == 0xfc00` → false
   - Link-local check `(first & 0xffc0) == 0xfe80` → false
   - Multicast check `(first & 0xff00) == 0xff00` → false
   - Documentation check → false
5. `is_blocked_ipv6` returns `false`, allowing the request through

The same bypass works for any private IPv4 address wrapped in IPv4-mapped IPv6 notation: `::ffff:127.0.0.1` (loopback), `::ffff:10.0.0.1` (private), `::ffff:192.168.1.1` (private), `::ffff:100.100.100.200` (OIDC metadata), etc.

**Impact:** Complete bypass of all IPv4 SSRF protections. An attacker with control over the OTel endpoint configuration can reach the AWS metadata endpoint (169.254.169.254), cloud OIDC endpoints (100.100.100.200), loopback services, and any private network address.

## Fix

Added a check at the start of `is_blocked_ipv6` using `Ipv6Addr::to_ipv4_mapped()` (stable since Rust 1.75, and this project uses nightly). When an IPv4-mapped IPv6 address is detected, the inner IPv4 address is extracted and delegated to `is_blocked_ipv4`, which already has comprehensive checks for all private/reserved IPv4 ranges.

Also added test cases for the five most critical bypass vectors:
- `::ffff:169.254.169.254` (AWS metadata)
- `::ffff:127.0.0.1` (loopback)
- `::ffff:10.0.0.1` (private)
- `::ffff:192.168.1.1` (private)
- `::ffff:100.100.100.200` (cloud OIDC)

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: anthonyshew <anthonyshew@gmail.com>
@anthonyshew anthonyshew merged commit 076ff97 into main May 26, 2026
41 checks passed
@anthonyshew anthonyshew deleted the shew/fix-otel-endpoint-validation branch May 26, 2026 22:15
anthonyshew pushed a commit that referenced this pull request May 26, 2026
## Release v2.9.15

> [!CAUTION]
> Versioned docs aliasing FAILED. [View
logs](https://github.com/vercel/turborepo/actions/runs/26478247978)

### Changes

- release(turborepo): 2.9.14 (#12805) (`da36727`)
- fix: Prune package.json workspaces (#12808) (`aea4138`)
- fix: Wait for process trees before task completion (#12809)
(`a3b4d94`)
- release(turborepo): 2.9.15-canary.1 (#12810) (`f7b9d3a`)
- ci: Sign macOS release binaries (#12811) (`fe3b84f`)
- release(turborepo): 2.9.15-canary.2 (#12812) (`c34a86b`)
- fix: Prevent cache archive symlink reads (#12813) (`ab90c81`)
- release(turborepo): 2.9.15-canary.3 (#12814) (`d92bfcb`)
- fix: Avoid path-racy chmod during directory restore (#12815)
(`c62c92b`)
- fix: Prevent cache restore symlink race writes (#12817) (`0f167cf`)
- chore: Deny Rust panic extraction by default (#12818) (`958fc4e`)
- fix: Make structured log symlink defense race-safe (#12821)
(`46df4de`)
- fix: Preserve Bun alias child packages (#12822) (`cbfef22`)
- fix: Avoid UTF-8 panics at boundaries (#12823) (`a9b43ba`)
- fix: Preserve non-UTF-8 Git path boundaries (#12826) (`85ba487`)
- fix: Create daemon dirs with private permissions (#12827) (`aca956e`)
- fix: Return Berry lockfile errors instead of panicking (#12828)
(`3fd29a3`)
- fix: Isolate Corepack state in integration tests (#12831) (`f49f23b`)
- ci: Use larger Windows runners for Rust tests (#12832) (`7618d6e`)
- docs: Add `with-vite-module-federation` example (#12794) (`2209f61`)
- test: Run Rust tests without partitioning (#12833) (`e53c512`)
- chore: Remove `TaskHashTracker`-based `expect()` calls (#12836)
(`a10a5fe`)
- chore: Deduplicate hash canonicalization (#12837) (`795a912`)
- fix: Prevent Windows process drain hangs (#12838) (`030f50b`)
- fix: Refactor execsync to execfilesync for Shell command built from
environment values (#12829) (`a410750`)
- test: Bound vt100 random quickcheck (#12839) (`8f9eac2`)
- fix: Validate daemon discovery responses (#12840) (`f3268b2`)
- fix: Store `PackageGraph` root invariants (#12841) (`67d733d`)
- chore: Avoid engine graph node expects (#12842) (`639c535`)
- test: Make Rust tests parallel-safe (#12843) (`dd34c30`)
- fix: Avoid graph utility node lookup panics (#12844) (`8beff2e`)
- fix: Avoid graph walker `expect()` calls (#12845) (`0734316`)
- fix: Remove fs panic extraction lints (#12846) (`d6396de`)
- fix: Remove fixed map panic extraction calls (#12847) (`412dc00`)
- fix: Remove devtools WebSocket panics (#12850) (`2d11941`)
- fix: Remove json rewrite panic lint allow (#12848) (`88709b4`)
- fix: Remove turborepo-types panic lint allows (#12849) (`9d2cda3`)
- chore: Remove turborepo-hash build expect (#12851) (`c271628`)
- fix: Remove napi panic lint allows (#12852) (`9d631fe`)
- fix: Avoid globwatch expect calls (#12853) (`800b355`)
- fix: Remove LSP expect callsites (#12854) (`5a22478`)
- fix: Remove scope panic lint allows (#12855) (`98cacad`)
- fix: Remove task hash panic lints (#12856) (`c727e30`)
- fix: Remove frameworks panic lint allows (#12857) (`6a5e891`)
- fix: Remove microfrontends proxy expect lint allow (#12859)
(`787eee6`)
- fix: Avoid API client expect calls (#12858) (`43d3229`)
- fix: Avoid task executor expect calls (#12860) (`709ebd2`)
- fix: Remove turbo-trace unwrap callsite (#12863) (`23ed3ac`)
- fix: Remove Vercel API mock expect usage (#12862) (`0386df2`)
- fix: Remove vt100 expect lint allow (#12861) (`db1ee55`)
- fix: Remove turborepo-shim expect callsites (#12864) (`6b7c2c7`)
- test: Deflake daemon existing process test (#12865) (`1c57b5b`)
- fix: Avoid repository NAPI unwrap calls (#12866) (`459d1e6`)
- fix: Remove pidlock panic callsites (#12867) (`aacfcc6`)
- fix: Remove telemetry panic callsites (#12868) (`9968f36`)
- chore: Remove Rust re-export shims (#12870) (`0c7b052`)
- fix: Remove turbo-json panic lint allows (#12869) (`3eb13fd`)
- fix: Remove `globwalk`'s `expect()` callsites (#12871) (`ca42137`)
- fix: Remove `turbopath`'s `expect()` callsites (#12872) (`e781dbe`)
- test: Deflake Corepack prepare lock on Windows (#12873) (`53c9b4b`)
- fix: Remove signals panic callsites (#12874) (`b5e3b6d`)
- fix: Remove turbo-trace expect allow (#12876) (`67657e5`)
- fix: Remove Vercel API mock unwrap usage (#12877) (`dd99f86`)
- fix: Remove task executor unwrap usage (#12878) (`f16c120`)
- fix: Remove run summary expect usage (#12879) (`2670768`)
- fix: Remove microfrontends proxy unwrap usage (#12880) (`80da7a6`)
- fix: Remove api client unwrap usage (#12881) (`73f3c1b`)
- fix: Remove globwalk unwrap usage (#12883) (`a058336`)
- fix: Remove UI `expect()` usage (#12882) (`843515e`)
- fix: Remove microfrontends expect usage (#12885) (`91d5ac0`)
- fix: Remove `boundaries`'s `expect()` usage (#12887) (`4ae4b19`)
- fix: Remove `turborepo-process`'s `unwrap()` usage (#12888)
(`7badbb5`)
- fix: Remove UI unwrap usage (#12889) (`8c4316e`)
- fix: Remove microfrontends unwrap allow (#12890) (`26168cd`)
- fix: Remove `turborepo-process`'s `expect()` usage (#12891)
(`f3e8a42`)
- fix: Remove scm expect usage (#12893) (`4c0a0e0`)
- fix: Remove auth unwrap usage (#12886) (`a2eed47`)
- fix: Remove `turbopath`'s `unwrap()` usage (#12884) (`e1f2003`)
- fix: Remove `auth`'s `expect()` usage (#12895) (`d13dee7`)
- fix: Remove wax unwrap usage (#12899) (`04c99fb`)
- fix: Remove scm unwrap usage (#12897) (`715cd2c`)
- fix: Remove `turborepo-boundaries`'s `unwrap()` usage (#12896)
(`4484b36`)
- fix: Remove daemon unwrap usage (#12898) (`643b982`)
- fix: Include lockfile-changed packages in affected tasks (#12900)
(`81cae94`)
- fix: Remove `turborepo-wax`'s `expect()` usage (#12901) (`18816eb`)
- fix: Remove `turborepo-filewatch`'s `expect()` usage (#12903)
(`d1dff11`)
- fix: Remove `turborepo-cache`'s `expect()` usage (#12902) (`ccd358d`)
- fix: Remove `turborepo-daemon`'s `expect()` usage (#12904) (`a9d8836`)
- fix: Remove `turborepo-engine`'s `unwrap()` usage (#12906) (`5262b40`)
- fix: Remove filewatch unwrap usage (#12907) (`364c801`)
- fix: Remove engine expect usage (#12908) (`92ef87c`)
- fix: Remove cache unwrap usage (#12909) (`c08053c`)
- fix: Remove `turborepo-lockfiles` `expect()` usage (#12910)
(`756ae7c`)
- chore: Set pnpm minimum release age (#12912) (`1636a8c`)
- fix: Remove `turborepo-lockfiles`'s `unwrap()` usage (#12911)
(`40f8d8f`)
- fix: Remove `turborepo-vt100`'s `unwrap()` usage (#12913) (`c7482f9`)
- release(turborepo): 2.9.15-canary.4 (#12905) (`9f289d9`)
- fix: Remove `turborepo-lib`'s `unwrap()` usage (#12915) (`a8ce590`)
- fix: Remove `turborepo-lib`'s `expect()` usage (#12914) (`d1745a6`)
- fix: Remove shim test unwrap usage (#12917) (`0d98ca3`)
- fix: Remove turbo json test unwrap allowance (#12918) (`01367e9`)
- fix: Remove run summary test unwrap usage (#12916) (`88745d1`)
- release(turborepo): 2.9.15-canary.5 (#12919) (`b44d419`)
- fix: Restore task completion semantics (#12923) (`1a71128`)
- fix: Preserve nested Bun workspace dependency versions (#12924)
(`a77a0e5`)
- release(turborepo): 2.9.15-canary.6 (#12925) (`f675858`)
- fix: Restore release PR auto-merge (#12927) (`155e672`)
- perf: Index repo gitignore matchers (#12928) (`187a0fd`)
- ci: Disable incremental Rust test builds (#12929) (`8c7dbc6`)
- perf: Trim OpenTelemetry crate features (#12930) (`7f0afe7`)
- perf: Trim microfrontends proxy HTTP features (#12931) (`ac537a8`)
- fix: Accept `experimentalCI` object config (#12934) (`6f662f2`)
- release(turborepo): 2.9.15-canary.7 (#12935) (`0e56cdc`)
- fix: Restore a few internal invariant checks (#12933) (`767a9d4`)
- fix: Improve profile tracing coverage (#12936) (`3063672`)
- fix: Use build-scale OTel duration buckets (#12939) (`6ed6fb0`)
- fix: Preserve pnpm injected peer package entries (#12940) (`31123f4`)
- feat: Add heap allocation profiling (#12943) (`c7ad6f2`)
- release(turborepo): 2.9.15-canary.8 (#12945) (`06e81ea`)
- docs: Correct attribute presence claims in turborepo-otel (#12932)
(`8fc94f3`)
- chore(turbo-codemod): remove duplicate "in" in transforms path comment
(#12948) (`5fa3039`)
- chore: Switch Geist font imports to npm geist package (#12952)
(`ebebf41`)
- fix: Respect root gitignore during prune (#12953) (`f96ccc4`)
- fix: Harden OTEL endpoint validation (#12954) (`076ff97`)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
anthonyshew pushed a commit that referenced this pull request May 27, 2026
## Release v2.9.16-canary.2

> [!CAUTION]
> Versioned docs aliasing FAILED. [View
logs](https://github.com/vercel/turborepo/actions/runs/26525563543)

### Changes

- release(turborepo): 2.9.15-canary.7 (#12935) (`0e56cdc`)
- fix: Restore a few internal invariant checks (#12933) (`767a9d4`)
- fix: Improve profile tracing coverage (#12936) (`3063672`)
- fix: Use build-scale OTel duration buckets (#12939) (`6ed6fb0`)
- fix: Preserve pnpm injected peer package entries (#12940) (`31123f4`)
- feat: Add heap allocation profiling (#12943) (`c7ad6f2`)
- release(turborepo): 2.9.15-canary.8 (#12945) (`06e81ea`)
- docs: Correct attribute presence claims in turborepo-otel (#12932)
(`8fc94f3`)
- chore(turbo-codemod): remove duplicate "in" in transforms path comment
(#12948) (`5fa3039`)
- chore: Switch Geist font imports to npm geist package (#12952)
(`ebebf41`)
- fix: Respect root gitignore during prune (#12953) (`f96ccc4`)
- fix: Harden OTEL endpoint validation (#12954) (`076ff97`)
- release(turborepo): 2.9.15 (#12955) (`c85d410`)
- fix: Avoid hanging PTY shutdown (#12958) (`52e81bd`)
- fix: Retry npm tlog publish failures (#12959) (`5317f65`)
- release(turborepo): 2.9.16-canary.1 (#12960) (`2284fa9`)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

[Security/Medium]: SSRF via experimental OpenTelemetry endpoint configuration

1 participant