[BUG] OIDC and logout flows unconditionally trust unvalidated X-Forwarded-Host/Proto headers in getPublicOrigin, causing Host header injection, open redirects, and 500 URL parse errors.

Open
#2,270 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
58/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
next.js, typescript

Research direction

Start by reading src/lib/oidc.ts around getPublicOrigin, then compare how src/lib/api/request-scheme.ts and src/lib/api/origin-check.ts gate forwarded headers. Trace the logout and OIDC callback redirects in the routes named in the issue, and check existing tests for origin handling. Done means untrusted forwarded headers cannot control auth redirect origins and comma-separated headers do not cause a 500.

Written by the indexing model from the issue text.

Description

bug

Description

In src/lib/oidc.ts, getPublicOrigin(request) derives the application's public origin by reading the x-forwarded-host and x-forwarded-proto request headers directly:

export function getPublicOrigin(request: Request): string {
  const forwardedHost = request.headers.get("x-forwarded-host");
  const forwardedProto = request.headers.get("x-forwarded-proto") || "https";
  const host = forwardedHost || request.headers.get("host");
  if (host) {
    return `${forwardedProto}://${host}`;
  }
  return new URL(request.url).origin;
}

This implementation contains three critical vulnerabilities:

  1. Bypasses Proxy Trust Decision: Across the rest of the application (src/lib/api/request-scheme.ts#L8-L19, src/lib/api/origin-check.ts#L72-L82, and src/lib/api/client-address.ts#L36), x-forwarded-host and x-forwarded-proto are explicitly treated as untrusted and gated behind readTrustProxyHeaders(). Even when an operator sets TRUST_PROXY_HEADERS=false to indicate LibreDB Studio is exposed directly without an intermediate reverse proxy, getPublicOrigin() ignores this setting and blindly trusts client-supplied headers.
  2. Open Redirect & Host Header Injection in Auth Flows: Because GET /api/auth/logout, GET /api/auth/oidc/login, and GET /api/auth/oidc/callback are public GET endpoints, they are exempt from state-changing CSRF origin checks (STATE_CHANGING in origin-check.ts only gates POST/PUT/PATCH/DELETE).
    • When a user logs out via src/app/api/auth/logout/route.ts#L89-L102, the route issues NextResponse.redirect(${origin}${withBasePath("/login")}). A malicious link carrying X-Forwarded-Host: attacker.com causes an unvalidated redirect to https://attacker.com/login (phishing vector).
    • In src/app/api/auth/oidc/callback/route.ts#L244, after completing OIDC authentication, the application executes NextResponse.redirect(${origin}${withBasePath(landing)}), redirecting the newly authenticated user to the attacker's domain.
  3. Unhandled 500 Denial-of-Service via Multi-Host Commas: When LibreDB Studio is behind standard reverse proxies (or when an attacker injects multiple headers), X-Forwarded-Host may contain comma-separated entries (e.g., X-Forwarded-Host: client-host.com, proxy-host.com). While origin-check.ts and request-scheme.ts parse this with .split(",")[0].trim(), getPublicOrigin() takes the whole string raw. This produces an invalid URL https://client-host.com, proxy-host.com/login, causing new URL(callbackUrl, origin) or NextResponse.redirect() to throw an uncaught TypeError: Invalid URL (HTTP 500), breaking login and logout for all users.

Steps to Reproduce

  1. Configure LibreDB Studio with OIDC enabled (NEXT_PUBLIC_AUTH_PROVIDER=oidc or enable SSO).
  2. Set TRUST_PROXY_HEADERS=false in environment variables.
  3. Send a logout request supplying an external X-Forwarded-Host header:
    curl -i "http://localhost:3000/api/auth/logout" \
      -H "X-Forwarded-Host: attacker-controlled-domain.com" \
      -H "X-Forwarded-Proto: https"
    
  4. Observe the Location response header:
    HTTP/1.1 307 Temporary Redirect
    Location: https://attacker-controlled-domain.com/login
    
  5. Next, send a request with standard comma-separated forwarded headers:
    curl -i "http://localhost:3000/api/auth/logout" \
      -H "X-Forwarded-Host: internal-proxy.com, edge-proxy.com"
    
  6. The request crashes with an unhandled 500 error due to TypeError: Invalid URL.

Expected Behavior

  • When TRUST_PROXY_HEADERS=false, X-Forwarded-Host and X-Forwarded-Proto must be completely ignored; the origin must be constructed only from the validated Host header or request.url.
  • X-Forwarded-Host must be split on , to read only the first proxy hop and validated against ALLOWED_ORIGINS or host formats.
  • Auth redirects (/api/auth/logout, /api/auth/oidc/callback) must never redirect to unvalidated external domains derived from client headers.

Actual Behavior

  • getPublicOrigin() unconditionally trusts X-Forwarded-Host regardless of TRUST_PROXY_HEADERS.
  • The user is redirected to the attacker-controlled origin.
  • Multi-value X-Forwarded-Host headers throw a TypeError: Invalid URL and crash the route with HTTP 500.

Environment

  • LibreDB Studio Version: Current main / master (commit a6cdee1+)
  • Browser: All
  • OS: macOS / Linux (Docker, Helm, Bare Metal)
  • Node.js/Bun Version: Bun 1.4.x / Node.js 22+

Additional Context

Affected files:

  • src/lib/oidc.ts
  • src/app/api/auth/logout/route.ts
  • src/app/api/auth/oidc/login/route.ts
  • src/app/api/auth/oidc/callback/route.ts

Compare with src/lib/api/request-scheme.ts#L10-L27, where the author correctly noted:

"Never base a security decision on this. x-forwarded-proto and x-forwarded-host are attacker-supplied unless a reverse proxy overwrites them... They are read here only behind readTrustProxyHeaders()."

Possible Solution

  1. In src/lib/oidc.ts, update getPublicOrigin(request: Request) to respect readTrustProxyHeaders():
    import { readTrustProxyHeaders } from "@/lib/security/config";
    
    export function getPublicOrigin(request: Request): string {
      let host: string | null = null;
      let proto = "https";
    
      if (readTrustProxyHeaders()) {
        const forwardedHost = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
        const forwardedProto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim().toLowerCase();
        if (forwardedHost) host = forwardedHost;
        if (forwardedProto === "http" || forwardedProto === "https") proto = forwardedProto;
      }
    
      if (!host) {
        host = request.headers.get("host")?.trim() || null;
      }
    
      if (host) {
        return `${proto}://${host}`;
      }
      return new URL(request.url).origin;
    }
    
  2. Validate that the computed origin or host matches ALLOWED_ORIGINS or request.url before using it in redirect targets.
Dominant language
TypeScript
Stars
810
Forks
158
Avg merge
9h 45m
Merged PRs (30d)
521

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from libredb/libredb-studio

All issues in libredb/libredb-studio

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.