Skip to content

fix(workers): stop edge proxies from following off-origin redirects - #1576

Merged
giswqs merged 4 commits into
opengeos:mainfrom
Ayush7614:harden/viewer-and-edge-proxy-safety
Jul 31, 2026
Merged

fix(workers): stop edge proxies from following off-origin redirects#1576
giswqs merged 4 commits into
opengeos:mainfrom
Ayush7614:harden/viewer-and-edge-proxy-safety

Conversation

@Ayush7614

@Ayush7614 Ayush7614 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Rewrite the viewer worker proxy to follow redirects only while they stay under https://geolibre.app/demo, refuse path traversal, and allow GET/HEAD only.
  • Teach the tiles worker to fetch allowlisted upstreams with the same host-bound redirect policy so OAM / Source Cooperative / OPM / USGS / Protomaps paths cannot be redirected off-origin.
  • Add unit coverage for both redirect policies.

Test plan

  • node --import tsx --test tests/edge-proxy-redirect.test.ts (6 passing)
  • npm run test:worker typechecks all workers
  • Deploy/preview the viewer worker and confirm a normal asset load still works
  • Confirm a mock upstream 302 to https://evil.example returns 502 instead of proxying the body

Summary by CodeRabbit

  • Security Improvements
    • Enforced allowlisted HTTPS upstreams for both viewer and tile proxying using allowed URL prefixes.
    • Hardened viewer path validation against traversal and encoded evasion attempts.
    • Updated redirect handling to follow only safe, in-scope targets within a hop limit; rejects unsafe methods and strips cookies/authorization plus upstream set-cookie headers.
  • Bug Fixes
    • Preserved correct 304 Not Modified pass-through (including ETag) instead of treating it as a redirect issue.
  • Tests
    • Expanded coverage for sanitization, allowlist enforcement, redirect success/failure scenarios, and redirect hop caps for both viewer and tiles.

Ayush7614 and others added 2 commits July 30, 2026 21:37
The viewer worker now follows redirects only while they stay under
https://geolibre.app/demo, and the tiles worker fetches allowlisted
upstreams with the same host-bound redirect policy so neither path can
be turned into an open proxy.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 300524d4-3d45-42d9-9cc8-893106d2e410

📥 Commits

Reviewing files that changed from the base of the PR and between 5494045 and 67baa7e.

📒 Files selected for processing (5)
  • tests/edge-proxy-redirect.test.ts
  • workers/tiles/src/allowlisted-fetch.ts
  • workers/tiles/src/index.ts
  • workers/viewer/src/index.ts
  • workers/viewer/src/proxy.ts
💤 Files with no reviewable changes (1)
  • workers/viewer/src/index.ts

📝 Walkthrough

Walkthrough

Viewer and tiles workers now enforce HTTPS upstream allowlists, bounded manual redirects, path validation, method restrictions, credential stripping, and sanitized responses. Tiles routes use the shared fetch helper, with tests covering traversal, origin, redirect, method, and hop-limit behavior.

Changes

Edge proxy security

Layer / File(s) Summary
Viewer proxy validation and redirect flow
workers/viewer/src/proxy.ts, workers/viewer/src/index.ts, tests/edge-proxy-redirect.test.ts
Viewer requests validate paths, methods, and HTTPS /demo destinations, follow bounded in-prefix redirects, strip credential-related headers, sanitize responses, and expose the proxy through the worker entrypoint.
Tiles allowlisted fetching and worker integration
workers/tiles/src/allowlisted-fetch.ts, workers/tiles/src/index.ts, tests/edge-proxy-redirect.test.ts
Tiles upstream requests use HTTPS prefix validation and bounded manual redirects across build, metadata, range, raster, and WMS routes; tests cover rejected off-host and accepted same-host redirects.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ViewerRequest
  participant proxyViewerRequest
  participant ViewerUpstream
  ViewerRequest->>proxyViewerRequest: request path and method
  proxyViewerRequest->>ViewerUpstream: sanitized HTTPS request
  ViewerUpstream-->>proxyViewerRequest: response or redirect Location
  proxyViewerRequest-->>ViewerRequest: sanitized response or 4xx/502
Loading
sequenceDiagram
  participant TilesRoute
  participant fetchAllowlistedUpstream
  participant TilesUpstream
  TilesRoute->>fetchAllowlistedUpstream: upstream URL and request options
  fetchAllowlistedUpstream->>TilesUpstream: manual HTTPS fetch
  TilesUpstream-->>fetchAllowlistedUpstream: response or redirect Location
  fetchAllowlistedUpstream-->>TilesRoute: final response or allowlist error
Loading

Poem

I’m a rabbit hopping through redirects bright,
Blocking stray origins from the night.
Paths stay clean, credentials hide,
Safe HTTPS hops remain inside.
Tests thump softly: “502, not today!”
And bounded routes are on their way.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: hardening worker proxy redirect handling to block off-origin redirects.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Ayush7614
Ayush7614 force-pushed the harden/viewer-and-edge-proxy-safety branch from f6bbdd6 to b97ae44 Compare July 30, 2026 16:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
workers/tiles/src/index.ts (1)

259-281: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unhandled probe error aborts the entire latest-build lookback.

fetchAllowlistedUpstream can now throw (disallowed redirect, hop-limit exceeded) where the previous plain fetch would not, since default fetch silently follows redirects. Because the call at Line 272 isn't wrapped in its own try/catch, a throw for any single day's probe aborts the whole for loop, and handlePmtilesRange's catch turns that into a hard "No recent Protomaps build available" 502 — even if other days in the lookback window (including today) are otherwise resolvable. This turns a per-day miss into a full-feature outage.

🛡️ Proposed fix: treat a failed probe as a miss, not a hard failure
   for (let i = 0; i <= LATEST_MAX_LOOKBACK_DAYS; i++) {
     const ymd = utcYmd(now - i * 86_400_000);
     // A one-byte range is the cheapest existence check. ...
-    const probe = await fetchAllowlistedUpstream(`${PMTILES_UPSTREAM}/${ymd}.pmtiles`, {
-      headers: { range: "bytes=0-0" },
-    });
+    let probe: Response;
+    try {
+      probe = await fetchAllowlistedUpstream(`${PMTILES_UPSTREAM}/${ymd}.pmtiles`, {
+        headers: { range: "bytes=0-0" },
+      });
+    } catch (err) {
+      console.warn(`Protomaps build probe failed for ${ymd}: ${String(err)}`);
+      continue;
+    }
     if (probe.status === 206) {
       latestCache = { date: ymd, at: now };
       return ymd;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workers/tiles/src/index.ts` around lines 259 - 281, Wrap the per-day
fetchAllowlistedUpstream call in resolveLatestBuildDate with a try/catch so
errors from an individual date’s probe are treated as misses and the lookback
loop continues. Preserve the existing 206 success path, allowing later dates to
resolve the latest build; only throw the existing “no recent Protomaps build
found” error after all dates fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@workers/tiles/src/allowlisted-fetch.ts`:
- Around line 38-64: Extract the duplicated manual redirect-validation loop from
fetchAllowlistedUpstream into a shared helper parameterized by the allowlist
predicate, then have workers/tiles/src/allowlisted-fetch.ts lines 38-64 and
workers/viewer/src/proxy.ts lines 70-124 use it while preserving their
worker-specific request logic. In tests/edge-proxy-redirect.test.ts lines
88-127, add a tiles-side redirect-hop-cap case mirroring the viewer coverage and
verify TILES_MAX_REDIRECT_HOPS is enforced.

---

Outside diff comments:
In `@workers/tiles/src/index.ts`:
- Around line 259-281: Wrap the per-day fetchAllowlistedUpstream call in
resolveLatestBuildDate with a try/catch so errors from an individual date’s
probe are treated as misses and the lookback loop continues. Preserve the
existing 206 success path, allowing later dates to resolve the latest build;
only throw the existing “no recent Protomaps build found” error after all dates
fail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bf5983d6-585b-4503-bbf2-d7f10ad5e90d

📥 Commits

Reviewing files that changed from the base of the PR and between 28032d0 and f6bbdd6.

📒 Files selected for processing (5)
  • tests/edge-proxy-redirect.test.ts
  • workers/tiles/src/allowlisted-fetch.ts
  • workers/tiles/src/index.ts
  • workers/viewer/src/index.ts
  • workers/viewer/src/proxy.ts

Comment thread workers/tiles/src/allowlisted-fetch.ts
@giswqs

giswqs commented Jul 30, 2026

Copy link
Copy Markdown
Member

/claude-review

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://382a00e1.geolibre-preview.pages.dev
Demo app https://382a00e1.geolibre-preview.pages.dev/demo/
Commit 67baa7e

Comment thread workers/tiles/src/index.ts Outdated
Comment thread workers/viewer/src/proxy.ts
Comment thread workers/tiles/src/allowlisted-fetch.ts Outdated
Comment thread workers/viewer/src/index.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor

All four inline comments posted successfully.

Code review

Bugs

  • None found — the redirect-following logic (manual redirect + hop cap + allowlist re-check per hop) in both workers/viewer/src/proxy.ts and workers/tiles/src/allowlisted-fetch.ts is internally consistent and matches the added unit tests.

Security

  • sanitizeViewerPath (workers/viewer/src/proxy.ts:20-28) blocks a literal .. segment and an encoded dot (%2e), but not an encoded slash (%2f) used to smuggle a .. segment past the /-based split — a path like /foo%2f..%2f..%2fsecret passes both checks. Exploitability depends on whether the upstream ever decodes %2f before resolving the path. Confidence: medium.
  • TILES_ALLOWED_UPSTREAM_HOSTS (workers/tiles/src/allowlisted-fetch.ts:7-15) allowlists redirects by hostname only. For the three S3 hostnames (shared, multi-tenant endpoints), this doesn't scope to the specific hardcoded buckets in DATASETS, so a compromised/misconfigured upstream could redirect to a different bucket on the same host and still pass. The module comment's "cannot turn the worker into an open proxy" claim doesn't fully hold for these entries. Confidence: medium.

Performance

  • No issues found; caching/edge behavior is unchanged by the redirect-policy refactor.

Quality

  • The } as RequestInit) casts at each cf-bearing call into fetchAllowlistedUpstream in workers/tiles/src/index.ts (e.g. line 375, and similarly at ~259/271/282) suggest the shared helper's init parameter isn't typed against Cloudflare's outgoing RequestInitCfProperties; the blanket cast bypasses excess-property checking, so a typo in a cf option would silently compile. Better fixed at the source (allowlisted-fetch.ts's type) than cast at each call site. Confidence: medium.
  • workers/viewer/src/index.ts:27-35 re-exports several proxy.ts symbols that don't appear to be consumed anywhere (tests import directly from ./proxy); likely unused surface left over from the refactor. Confidence: low.

CLAUDE.md

  • No violations — no new external tile/map hosts were introduced (the allowlists mirror hosts already in use), so no Tauri CSP update is required.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site Deploy failed. See the job log.
Demo app Unavailable
Commit 67baa7e

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/edge-proxy-redirect.test.ts`:
- Around line 35-65: Extend the “ok” branch of the viewer redirect policy test
to return set-cookie and/or set-cookie2 headers from the mock upstream response,
then assert those headers are absent from the final ok response. Keep the
existing redirect assertions unchanged and ensure the test covers
sanitizeUpstreamResponse’s cookie-stripping behavior.

In `@workers/tiles/src/allowlisted-fetch.ts`:
- Around line 7-15: Update isAllowedTilesUpstreamUrl to validate both the
allowlisted S3 hostname and the approved dataset path prefix before following
redirects; do not permit arbitrary bucket/object paths on shared
s3*.amazonaws.com hosts. Reuse the resolved DATASETS prefix for OPM datasets
where available, while preserving existing allowlist behavior for non-S3
upstream hosts.

In `@workers/viewer/src/proxy.ts`:
- Around line 96-124: Update the redirect handling in the proxy flow to treat
304 Not Modified as a non-redirect response, allowing it to pass through
sanitizeUpstreamResponse(response) like other non-redirect statuses. Keep
301–303 and 307–308 responses subject to Location validation and redirect-hop
processing.
- Around line 20-28: Update sanitizeViewerPath to reject both lowercase and
uppercase encoded slashes (%2f and %2F) alongside the existing encoded-dot and
backslash checks. Preserve the current literal .. segment validation and return
behavior; if normalization is introduced, perform it before checking traversal
segments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4841c3e8-5599-4bab-ac1f-99f4f859656e

📥 Commits

Reviewing files that changed from the base of the PR and between f6bbdd6 and b97ae44.

📒 Files selected for processing (5)
  • tests/edge-proxy-redirect.test.ts
  • workers/tiles/src/allowlisted-fetch.ts
  • workers/tiles/src/index.ts
  • workers/viewer/src/index.ts
  • workers/viewer/src/proxy.ts

Comment thread tests/edge-proxy-redirect.test.ts
Comment thread workers/tiles/src/allowlisted-fetch.ts Outdated
Comment thread workers/viewer/src/proxy.ts
Comment thread workers/viewer/src/proxy.ts
Pass through 304 responses, reject encoded-slash traversal, scope S3
redirects to known dataset prefixes, harden Protomaps probes, and expand
redirect/cookie coverage.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/edge-proxy-redirect.test.ts (1)

143-178: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a tiles-side 304 regression test.

The tiles helper now explicitly passes through 304 responses, but this suite only locks that behavior down for the viewer. A future broad 3xx redirect check in fetchAllowlistedUpstream would pass these 200/302 tests while breaking conditional revalidation again. Add a 304 mock and assert its etag is returned.

As per coding guidelines, test changes must maintain the configured coverage floors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/edge-proxy-redirect.test.ts` around lines 143 - 178, Add a tiles-side
regression test in the existing fetchAllowlistedUpstream test coverage that
mocks a 304 response with an ETag and asserts the response remains 304 with the
ETag preserved. Ensure the test exercises the tiles helper path rather than only
the viewer behavior, and keep the added coverage within the configured floors.

Source: Coding guidelines

workers/viewer/src/proxy.ts (1)

45-52: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Reject alternate ports in allowed redirect URLs.

isAllowedUpstreamUrl() checks parsed.hostname, so allowed redirects can include non-default ports such as https://geolibre.app:8443/demo/.... Cloudflare Workers ignores non-default ports in outbound fetches, which can cause the redirect path to be routed differently than intended. Return false when parsed.port is present, and add a regression case for alternate ports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workers/viewer/src/proxy.ts` around lines 45 - 52, Update
isAllowedUpstreamUrl() to reject URLs when parsed.port is non-empty, while
preserving the existing HTTPS, hostname, and /demo path checks. Add a regression
test covering an allowed hostname with an alternate port and assert it returns
false.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/edge-proxy-redirect.test.ts`:
- Around line 143-178: Add a tiles-side regression test in the existing
fetchAllowlistedUpstream test coverage that mocks a 304 response with an ETag
and asserts the response remains 304 with the ETag preserved. Ensure the test
exercises the tiles helper path rather than only the viewer behavior, and keep
the added coverage within the configured floors.

In `@workers/viewer/src/proxy.ts`:
- Around line 45-52: Update isAllowedUpstreamUrl() to reject URLs when
parsed.port is non-empty, while preserving the existing HTTPS, hostname, and
/demo path checks. Add a regression test covering an allowed hostname with an
alternate port and assert it returns false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: aabc5faf-e0de-49eb-9f16-c15b07ac0eb7

📥 Commits

Reviewing files that changed from the base of the PR and between b97ae44 and 5494045.

📒 Files selected for processing (5)
  • tests/edge-proxy-redirect.test.ts
  • workers/tiles/src/allowlisted-fetch.ts
  • workers/tiles/src/index.ts
  • workers/viewer/src/index.ts
  • workers/viewer/src/proxy.ts
💤 Files with no reviewable changes (1)
  • workers/viewer/src/index.ts

Block non-default ports on viewer redirect allowlist and lock tiles
conditional-revalidation passthrough with an ETag regression test.
@Ayush7614
Ayush7614 force-pushed the harden/viewer-and-edge-proxy-safety branch from 5494045 to 67baa7e Compare July 30, 2026 20:44
@giswqs
giswqs merged commit e910e01 into opengeos:main Jul 31, 2026
25 checks passed
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.

2 participants