apply security fix
Build & Push / Build & Push Docker Image (push) Failing after 3m46s

This commit is contained in:
root
2026-07-19 22:22:33 -04:00
parent ec03e783e5
commit a010ad6811
41 changed files with 626 additions and 224 deletions
@@ -4,6 +4,17 @@ type RouteContext = {
params: Promise<{ path?: string[] }> | { path?: string[] }
}
const SPOOFABLE_FORWARDING_HEADERS = new Set([
'forwarded',
'x-forwarded-for',
'x-forwarded-host',
'x-forwarded-proto',
'x-real-ip',
'cf-connecting-ip',
'true-client-ip',
'x-client-ip',
])
const HOP_BY_HOP_HEADERS = new Set([
'connection',
'content-encoding',
@@ -51,7 +62,8 @@ function getSetCookieHeaders(headers: Headers): string[] {
function copyRequestHeaders(request: Request): Headers {
const headers = new Headers()
request.headers.forEach((value, key) => {
if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) headers.set(key, value)
const normalized = key.toLowerCase()
if (!HOP_BY_HOP_HEADERS.has(normalized) && !SPOOFABLE_FORWARDING_HEADERS.has(normalized)) headers.set(key, value)
})
return headers
}
@@ -59,7 +71,8 @@ function copyRequestHeaders(request: Request): Headers {
function copyResponseHeaders(upstream: Response): Headers {
const headers = new Headers()
upstream.headers.forEach((value, key) => {
if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) headers.set(key, value)
const normalized = key.toLowerCase()
if (!HOP_BY_HOP_HEADERS.has(normalized) && !SPOOFABLE_FORWARDING_HEADERS.has(normalized)) headers.set(key, value)
})
return headers
}
+3 -3
View File
@@ -88,7 +88,7 @@ describe('dashboard middleware', () => {
expect(response).toEqual({ kind: 'redirect', url: 'https://rentaldrivego.example/dashboard/sign-in?redirect=%2Fdashboard%2Freservations' })
})
it('uses trusted forwarded host/proto when building the dashboard sign-in redirect', async () => {
it('ignores spoofed forwarded host/proto when building the dashboard sign-in redirect', async () => {
const { default: middleware } = await loadMiddleware('https://market.example.com')
const response = middleware(request('http://dashboard:3001/dashboard/billing', {
@@ -98,7 +98,7 @@ describe('dashboard middleware', () => {
},
}) as never)
expect(response).toEqual({ kind: 'redirect', url: 'https://workspace.customer.example/dashboard/sign-in?redirect=%2Fdashboard%2Fbilling' })
expect(response).toEqual({ kind: 'redirect', url: 'https://market.example.com/dashboard/sign-in?redirect=%2Fdashboard%2Fbilling' })
})
it('ignores internal forwarded hosts when building the dashboard sign-in redirect', async () => {
@@ -119,7 +119,7 @@ describe('dashboard middleware', () => {
const response = middleware(request('https://workspace.example.com/dashboard/sign-in?redirect=/dashboard/fleet', { token: 'employee-token' }) as never)
expect(response).toEqual({ kind: 'redirect', url: 'https://workspace.example.com/dashboard' })
expect(response).toEqual({ kind: 'redirect', url: 'https://market.example.com/dashboard' })
})
it('allows public dashboard auth pages without a token', async () => {
+7 -20
View File
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const WEBSITE_URL = process.env.NEXT_PUBLIC_WEBSITE_URL ?? 'http://localhost:3000'
const DASHBOARD_PUBLIC_URL = process.env.NEXT_PUBLIC_DASHBOARD_URL ?? `${WEBSITE_URL.replace(/\/$/, '')}/dashboard`
const DASHBOARD_BASE_PATH = '/dashboard'
function toDashboardAppPath(pathname: string): string {
@@ -29,26 +30,12 @@ function deduplicatePublicDashboardPath(pathname: string): string | null {
return normalized || DASHBOARD_BASE_PATH
}
function resolveProxyUrl(req: NextRequest, pathname: string): URL {
const forwardedHost = req.headers.get('x-forwarded-host')
const forwardedProto = req.headers.get('x-forwarded-proto')
const websiteOrigin = new URL(WEBSITE_URL)
const url = new URL(pathname, req.nextUrl.origin)
if (forwardedHost && !isInternalHost(forwardedHost)) {
url.host = forwardedHost
url.protocol = (forwardedProto ?? 'http') + ':'
if (!hasExplicitPort(forwardedHost) || isInternalAppPort(url.port)) url.port = ''
} else if (!isInternalHost(req.nextUrl.host)) {
url.host = req.nextUrl.host
url.protocol = req.nextUrl.protocol
if (!hasExplicitPort(req.nextUrl.host) || isInternalAppPort(url.port)) url.port = ''
} else {
url.host = websiteOrigin.host
url.protocol = websiteOrigin.protocol
if (!hasExplicitPort(websiteOrigin.host) || isInternalAppPort(url.port)) url.port = ''
}
return url
function resolveProxyUrl(_req: NextRequest, pathname: string): URL {
const canonicalDashboard = new URL(DASHBOARD_PUBLIC_URL)
const publicPath = pathname.startsWith(DASHBOARD_BASE_PATH)
? pathname
: toPublicDashboardPath(pathname)
return new URL(publicPath, canonicalDashboard.origin)
}
function hasExplicitPort(host: string): boolean {