Skip to content

Fix localhost download redirects behind reverse proxies#498

Merged
jamiepine merged 1 commit intojamiepine:mainfrom
shekharyv:fix/download-localhost-redirect
Apr 19, 2026
Merged

Fix localhost download redirects behind reverse proxies#498
jamiepine merged 1 commit intojamiepine:mainfrom
shekharyv:fix/download-localhost-redirect

Conversation

@shekharyv
Copy link
Copy Markdown
Contributor

@shekharyv shekharyv commented Apr 19, 2026

Fixes incorrect download redirects that can point users to internal origins like https://localhost:8080/download?... in production.

What changed

  • Updated landing/src/app/download/[platform]/route.ts
  • Added getPublicOrigin(request) helper that prefers:
    • x-forwarded-proto
    • x-forwarded-host
  • Redirect targets now use the public origin instead of request.url origin when behind a proxy/CDN.

Why

In some deployments, request.url carries an internal upstream origin (for example localhost). Using forwarded headers ensures end-users stay on the public domain.

Notes

  • No installer/runtime behavior changes.
  • This is a redirect-host correctness fix only.

Fixes #496

Summary by CodeRabbit

  • Bug Fixes
    • Fixed redirect routing to correctly direct users to platform-specific installation and download pages.

Prefer x-forwarded host/proto for redirect URL construction so users are not sent to internal localhost origins.

Fixes jamiepine#496
Copilot AI review requested due to automatic review settings April 19, 2026 17:48
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

A targeted fix that corrects download redirect URLs by properly detecting the public origin through proxy/CDN headers (x-forwarded-proto and x-forwarded-host), falling back to the request URL origin when headers are absent.

Changes

Cohort / File(s) Summary
Public Origin Helper & Redirect Fix
landing/src/app/download/[platform]/route.ts
Added getPublicOrigin() helper function to extract externally visible origin from request headers or URL. Updated GET handler to use this function when constructing redirect destinations for /linux-install (Linux) and /download?platform=... (other platforms), resolving localhost URL issues in proxy scenarios.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A wayward link once pointed home,
To localhost's lonely tome,
Now headers guide the rabbit's way,
Public origins save the day! 🔗✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: fixing localhost redirects in reverse proxy scenarios, which directly addresses the pull request's core objective.
Linked Issues check ✅ Passed The code changes implement the solution to issue #496 by adding getPublicOrigin() to redirect to public domain instead of localhost/internal origins when behind proxies.
Out of Scope Changes check ✅ Passed All changes are directly related to the stated objective of fixing download redirects in proxy scenarios; no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes incorrect redirect origins for the /download/[platform] route when the app is deployed behind a reverse proxy/CDN, preventing redirects to internal upstream origins (e.g. localhost).

Changes:

  • Added a getPublicOrigin(request) helper that prefers x-forwarded-proto + x-forwarded-host.
  • Updated download and linux-install redirects to use the computed public origin instead of request.url.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +18 to +27
function getPublicOrigin(request: NextRequest): string {
const forwardedHost = request.headers.get('x-forwarded-host');
const forwardedProto = request.headers.get('x-forwarded-proto');

if (forwardedHost && forwardedProto) {
// Behind reverse proxies/CDNs, request.url can be an internal origin
// (for example localhost:8080). Prefer forwarded headers so redirects
// keep users on the public domain.
return `${forwardedProto}://${forwardedHost}`;
}
Comment on lines +19 to +27
const forwardedHost = request.headers.get('x-forwarded-host');
const forwardedProto = request.headers.get('x-forwarded-proto');

if (forwardedHost && forwardedProto) {
// Behind reverse proxies/CDNs, request.url can be an internal origin
// (for example localhost:8080). Prefer forwarded headers so redirects
// keep users on the public domain.
return `${forwardedProto}://${forwardedHost}`;
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@landing/src/app/download/`[platform]/route.ts:
- Around line 18-30: The getPublicOrigin function currently trusts
x-forwarded-host and x-forwarded-proto directly; fix it by normalizing and
validating these headers before use: extract the first comma-separated value
from request.headers.get('x-forwarded-host') and
request.headers.get('x-forwarded-proto'), trim them, ensure proto is exactly
"http" or "https", and validate the host against a configured allowlist (from an
env var) of trusted public hosts; only when both normalized proto and host pass
validation should you return `${proto}://${host}`, otherwise fall back to new
URL(request.url).origin. Ensure you reference and update getPublicOrigin and the
header reads to perform these checks and rejection logic.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: c59ff1e7-6ae2-49c9-b7a8-1acfeb785f90

📥 Commits

Reviewing files that changed from the base of the PR and between 27a5a62 and c3e7dc6.

📒 Files selected for processing (1)
  • landing/src/app/download/[platform]/route.ts

Comment on lines +18 to +30
function getPublicOrigin(request: NextRequest): string {
const forwardedHost = request.headers.get('x-forwarded-host');
const forwardedProto = request.headers.get('x-forwarded-proto');

if (forwardedHost && forwardedProto) {
// Behind reverse proxies/CDNs, request.url can be an internal origin
// (for example localhost:8080). Prefer forwarded headers so redirects
// keep users on the public domain.
return `${forwardedProto}://${forwardedHost}`;
}

return new URL(request.url).origin;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Look for existing forwarded-header handling, trusted-host config,
# or canonical public-origin configuration relevant to download redirects.

set -euo pipefail

rg -n -C3 '\b(x-forwarded-host|x-forwarded-proto|Forwarded|trustedHosts|allowedHosts|DOWNLOAD_PUBLIC_HOSTS|NEXT_PUBLIC_SITE|PUBLIC_URL|SITE_URL|BASE_URL)\b' \
  --glob '!**/node_modules/**' \
  --glob '!**/.next/**' \
  --glob '!**/dist/**' \
  --glob '!**/build/**' || true

fd -i '^(next\.config\.(js|mjs|ts)|middleware\.(ts|js)|vercel\.json|Caddyfile|.*nginx.*|.*traefik.*|docker-compose.*|.*\.conf)$' \
  -x sh -c 'printf "\n--- %s ---\n" "$1"; sed -n "1,180p" "$1"' sh {}

Repository: jamiepine/voicebox

Length of output: 3009


🏁 Script executed:

cat -n landing/src/app/download/[platform]/route.ts

Repository: jamiepine/voicebox

Length of output: 2100


🏁 Script executed:

fd -type f -name "middleware*" landing/ || echo "No middleware files found"

Repository: jamiepine/voicebox

Length of output: 275


🏁 Script executed:

rg -n "getPublicOrigin" landing/src/app/download/[platform]/route.ts -A 10 -B 2

Repository: jamiepine/voicebox

Length of output: 1160


Validate forwarded headers before using them as a redirect origin.

Lines 19–20 and 26 read and use x-forwarded-host and x-forwarded-proto headers directly without validation. If an upstream reverse proxy misconfigures or fails to strip client-supplied forwarded headers, this becomes an open redirect vulnerability—attackers can craft requests with x-forwarded-host: attacker.com to redirect users to arbitrary domains. Additionally, comma-separated proxy chains are not handled, and invalid protocols are not restricted.

Normalize the headers to extract the first value, restrict to http/https only, and require an allowlist of trusted public hosts via environment configuration.

🛡️ Proposed hardening
+const ALLOWED_PUBLIC_HOSTS = new Set(
+  (process.env.DOWNLOAD_PUBLIC_HOSTS ?? '')
+    .split(',')
+    .map((host) => host.trim().toLowerCase())
+    .filter(Boolean),
+);
+
+function firstForwardedValue(value: string | null): string | null {
+  return value?.split(',')[0]?.trim() || null;
+}
+
 function getPublicOrigin(request: NextRequest): string {
-  const forwardedHost = request.headers.get('x-forwarded-host');
-  const forwardedProto = request.headers.get('x-forwarded-proto');
+  const forwardedHost = firstForwardedValue(request.headers.get('x-forwarded-host'));
+  const forwardedProto = firstForwardedValue(request.headers.get('x-forwarded-proto'));
+  const fallbackOrigin = new URL(request.url).origin;
 
   if (forwardedHost && forwardedProto) {
     // Behind reverse proxies/CDNs, request.url can be an internal origin
     // (for example localhost:8080). Prefer forwarded headers so redirects
     // keep users on the public domain.
-    return `${forwardedProto}://${forwardedHost}`;
+    try {
+      const origin = new URL(`${forwardedProto}://${forwardedHost}`);
+      const isHttpOrigin = origin.protocol === 'https:' || origin.protocol === 'http:';
+      const isAllowedHost = ALLOWED_PUBLIC_HOSTS.has(origin.host.toLowerCase());
+
+      if (isHttpOrigin && isAllowedHost) {
+        return origin.origin;
+      }
+    } catch {
+      // Fall back below when forwarded headers are malformed.
+    }
   }
 
-  return new URL(request.url).origin;
+  return fallbackOrigin;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getPublicOrigin(request: NextRequest): string {
const forwardedHost = request.headers.get('x-forwarded-host');
const forwardedProto = request.headers.get('x-forwarded-proto');
if (forwardedHost && forwardedProto) {
// Behind reverse proxies/CDNs, request.url can be an internal origin
// (for example localhost:8080). Prefer forwarded headers so redirects
// keep users on the public domain.
return `${forwardedProto}://${forwardedHost}`;
}
return new URL(request.url).origin;
}
const ALLOWED_PUBLIC_HOSTS = new Set(
(process.env.DOWNLOAD_PUBLIC_HOSTS ?? '')
.split(',')
.map((host) => host.trim().toLowerCase())
.filter(Boolean),
);
function firstForwardedValue(value: string | null): string | null {
return value?.split(',')[0]?.trim() || null;
}
function getPublicOrigin(request: NextRequest): string {
const forwardedHost = firstForwardedValue(request.headers.get('x-forwarded-host'));
const forwardedProto = firstForwardedValue(request.headers.get('x-forwarded-proto'));
const fallbackOrigin = new URL(request.url).origin;
if (forwardedHost && forwardedProto) {
// Behind reverse proxies/CDNs, request.url can be an internal origin
// (for example localhost:8080). Prefer forwarded headers so redirects
// keep users on the public domain.
try {
const origin = new URL(`${forwardedProto}://${forwardedHost}`);
const isHttpOrigin = origin.protocol === 'https:' || origin.protocol === 'http:';
const isAllowedHost = ALLOWED_PUBLIC_HOSTS.has(origin.host.toLowerCase());
if (isHttpOrigin && isAllowedHost) {
return origin.origin;
}
} catch {
// Fall back below when forwarded headers are malformed.
}
}
return fallbackOrigin;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/app/download/`[platform]/route.ts around lines 18 - 30, The
getPublicOrigin function currently trusts x-forwarded-host and x-forwarded-proto
directly; fix it by normalizing and validating these headers before use: extract
the first comma-separated value from request.headers.get('x-forwarded-host') and
request.headers.get('x-forwarded-proto'), trim them, ensure proto is exactly
"http" or "https", and validate the host against a configured allowlist (from an
env var) of trusted public hosts; only when both normalized proto and host pass
validation should you return `${proto}://${host}`, otherwise fall back to new
URL(request.url).origin. Ensure you reference and update getPublicOrigin and the
header reads to perform these checks and rejection logic.

@jamiepine jamiepine merged commit e3f7cd9 into jamiepine:main Apr 19, 2026
4 of 5 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.

The download links aren't working.

3 participants