mirror of
https://github.com/mudler/LocalAI.git
synced 2026-05-16 20:52:08 -04:00
* fix(http): honor X-Forwarded-Prefix when proxy strips the prefix Closes #9145. Two related issues kept the React UI from loading when a reverse proxy rewrites a sub-path with prefix-stripping (e.g. Caddy `handle_path`): 1. `BaseURL` only computed a prefix from the path StripPathPrefix had removed, so when the proxy strips the prefix before forwarding, the request arrives without it and the base URL was returned without a prefix. Extract a `BasePathPrefix` helper and add an `X-Forwarded-Prefix` header fallback so the prefix is recovered. 2. `<base href>` only changes how relative URLs resolve; the build emits path-absolute references like `/assets/...` and `/favicon.svg`, which still resolve against the origin and bypass the proxy prefix. Rewrite those references in the served `index.html` so the browser requests them through the proxy. Adds unit coverage for `BaseURL` with a pre-stripped path and an end-to-end test for the proxy-stripped scenario. Assisted-by: Claude:claude-opus-4-7 * fix(http): gate X-Forwarded-Prefix through SafeForwardedPrefix in BasePathPrefix BasePathPrefix consumed X-Forwarded-Prefix directly, so a value the codebase elsewhere rejects (e.g. "//evil.com") slipped through and was interpolated into the SPA index.html — both into the path-absolute asset URL rewrite in serveIndex (turning "/assets/..." into "//evil.com/assets/...", a protocol-relative URL that loads JS from a foreign origin) and into <base href>. Route the header through the existing SafeForwardedPrefix validator that StripPathPrefix and prefixRedirect already use, and HTML-escape the prefix before injecting it into the asset rewrite as defense in depth against attribute breakout. Tests cover //evil.com, backslashes, control chars, CR/LF and a missing leading slash; the integration test asserts an unsafe prefix can't poison asset URLs. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: claude-code:claude-opus-4-7-1m [Read] [Edit] [Bash] --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
72 lines
2.3 KiB
Go
72 lines
2.3 KiB
Go
package middleware
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// BasePathPrefix returns the URL path prefix that the request was reached
|
|
// under (e.g. "/myprefix/"). It always returns a value that starts and ends
|
|
// with `/`, defaulting to "/" when the app is not behind a path prefix.
|
|
//
|
|
// It first looks at the path StripPathPrefix removed (when the proxy forwards
|
|
// the prefix in the URL), then falls back to the X-Forwarded-Prefix header
|
|
// (when the proxy strips the prefix before forwarding, e.g. Caddy's
|
|
// handle_path).
|
|
//
|
|
// The header fallback is gated through SafeForwardedPrefix because the value
|
|
// flows into the SPA HTML response (both <base href> and the path-absolute
|
|
// asset URL rewrite in serveIndex). X-Forwarded-Prefix is attacker
|
|
// controllable on misconfigured proxy chains; without that gate a value like
|
|
// "//evil.com" turns the asset rewrite into a protocol-relative URL that
|
|
// loads JS from a foreign origin.
|
|
func BasePathPrefix(c echo.Context) string {
|
|
path := c.Path()
|
|
origPath := c.Request().URL.Path
|
|
|
|
if storedPath, ok := c.Get("_original_path").(string); ok && storedPath != "" {
|
|
origPath = storedPath
|
|
}
|
|
|
|
if path != origPath && strings.HasSuffix(origPath, path) && len(path) > 0 {
|
|
prefixLen := len(origPath) - len(path)
|
|
if prefixLen > 0 {
|
|
pathPrefix := origPath[:prefixLen]
|
|
if !strings.HasSuffix(pathPrefix, "/") {
|
|
pathPrefix += "/"
|
|
}
|
|
return pathPrefix
|
|
}
|
|
}
|
|
|
|
if validated, ok := SafeForwardedPrefix(c.Request().Header.Get("X-Forwarded-Prefix")); ok {
|
|
if !strings.HasSuffix(validated, "/") {
|
|
validated += "/"
|
|
}
|
|
return validated
|
|
}
|
|
|
|
return "/"
|
|
}
|
|
|
|
// BaseURL returns the base URL for the given HTTP request context.
|
|
// It takes into account that the app may be exposed by a reverse-proxy under a different protocol, host and path.
|
|
// The returned URL is guaranteed to end with `/`.
|
|
// The method should be used in conjunction with the StripPathPrefix middleware.
|
|
func BaseURL(c echo.Context) string {
|
|
scheme := "http"
|
|
if c.Request().Header.Get("X-Forwarded-Proto") == "https" {
|
|
scheme = "https"
|
|
} else if c.Request().TLS != nil {
|
|
scheme = "https"
|
|
}
|
|
|
|
host := c.Request().Host
|
|
if forwardedHost := c.Request().Header.Get("X-Forwarded-Host"); forwardedHost != "" {
|
|
host = forwardedHost
|
|
}
|
|
|
|
return scheme + "://" + host + BasePathPrefix(c)
|
|
}
|