mirror of
https://github.com/mudler/LocalAI
synced 2026-05-24 09:28:23 +00:00
Some checks are pending
lint / golangci-lint (push) Waiting to run
Security Scan / tests (push) Waiting to run
tests-aio / tests-aio (push) Waiting to run
build backend container images / generate-matrix (push) Waiting to run
build backend container images / backend-jobs-multiarch (push) Blocked by required conditions
build backend container images / backend-jobs-singlearch (push) Blocked by required conditions
build backend container images / backend-merge-jobs-multiarch (push) Blocked by required conditions
build backend container images / backend-merge-jobs-singlearch (push) Blocked by required conditions
build backend container images / backend-jobs-darwin (push) Blocked by required conditions
Build test / build-test (push) Waiting to run
Build test / launcher-build-darwin (push) Waiting to run
Build test / launcher-build-linux (push) Waiting to run
Explorer deployment / build-linux (push) Waiting to run
GPU tests / ubuntu-latest (1.21.x) (push) Waiting to run
generate and publish intel docker caches / generate_caches (intel/oneapi-basekit:2025.3.2-0-devel-ubuntu24.04, linux/amd64, arc-runner-set) (push) Waiting to run
Deploy docs to GitHub Pages / build (push) Waiting to run
Deploy docs to GitHub Pages / deploy (push) Blocked by required conditions
build container images / core-image-build (ubuntu:24.04, vulkan, --jobs=4 --output-sync=target, amd64, linux/amd64, ubuntu-latest, false, auto, -gpu-vulkan, noble, 2404) (push) Waiting to run
build container images / core-image-build (ubuntu:24.04, vulkan, --jobs=4 --output-sync=target, arm64, linux/arm64, ubuntu-24.04-arm, false, auto, -gpu-vulkan, noble, 2404) (push) Waiting to run
build container images / core-image-merge (push) Blocked by required conditions
build container images / gpu-vulkan-image-merge (push) Blocked by required conditions
build container images / hipblas-jobs (rocm/dev-ubuntu-24.04:7.2.1, hipblas, --jobs=3 --output-sync=target, linux/amd64, ubuntu-latest, auto, -gpu-hipblas, noble, 2404) (push) Waiting to run
build container images / core-image-build (intel/oneapi-basekit:2025.3.2-0-devel-ubuntu24.04, intel, --jobs=3 --output-sync=target, linux/amd64, ubuntu-latest, auto, -gpu-intel, noble, 2404) (push) Waiting to run
build container images / core-image-build (ubuntu:22.04, cublas, 13, 0, --jobs=4 --output-sync=target, linux/amd64, ubuntu-latest, false, auto, -gpu-nvidia-cuda-13, noble, 2404) (push) Waiting to run
build container images / core-image-build (ubuntu:24.04, , --jobs=4 --output-sync=target, amd64, linux/amd64, ubuntu-latest, false, auto, , noble, 2404) (push) Waiting to run
build container images / core-image-build (ubuntu:24.04, , --jobs=4 --output-sync=target, arm64, linux/arm64, ubuntu-24.04-arm, false, auto, , noble, 2404) (push) Waiting to run
build container images / core-image-build (ubuntu:24.04, cublas, 12, 8, --jobs=4 --output-sync=target, linux/amd64, ubuntu-latest, false, auto, -gpu-nvidia-cuda-12, noble, 2404) (push) Waiting to run
build container images / gh-runner (nvcr.io/nvidia/l4t-jetpack:r36.4.0, cublas, 12, 0, --jobs=4 --output-sync=target, linux/arm64, ubuntu-24.04-arm, true, auto, -nvidia-l4t-arm64, jammy, 2204) (push) Waiting to run
build container images / gh-runner (ubuntu:24.04, cublas, 13, 0, --jobs=4 --output-sync=target, linux/arm64, ubuntu-24.04-arm, false, auto, -nvidia-l4t-arm64-cuda-13, noble, 2404) (push) Waiting to run
Tests extras backends / tests-sherpa-onnx-grpc-transcription (push) Blocked by required conditions
Tests extras backends / tests-whisper-grpc-transcription (push) Blocked by required conditions
Tests extras backends / tests-sherpa-onnx-grpc-tts (push) Blocked by required conditions
Tests extras backends / tests-ik-llama-cpp-grpc (push) Blocked by required conditions
Tests extras backends / tests-turboquant-grpc (push) Blocked by required conditions
Tests extras backends / tests-coqui (push) Blocked by required conditions
Tests extras backends / tests-diffusers (push) Blocked by required conditions
Tests extras backends / detect-changes (push) Waiting to run
Tests extras backends / tests-transformers (push) Blocked by required conditions
Tests extras backends / tests-rerankers (push) Blocked by required conditions
Tests extras backends / tests-moonshine (push) Blocked by required conditions
Tests extras backends / tests-pocket-tts (push) Blocked by required conditions
Tests extras backends / tests-voxcpm (push) Blocked by required conditions
Tests extras backends / tests-liquid-audio (push) Blocked by required conditions
Tests extras backends / tests-llama-cpp-quantization (push) Blocked by required conditions
Tests extras backends / tests-llama-cpp-grpc (push) Blocked by required conditions
Tests extras backends / tests-llama-cpp-grpc-transcription (push) Blocked by required conditions
Tests extras backends / tests-llama-cpp-smoke (push) Waiting to run
Tests extras backends / tests-sherpa-onnx-realtime (push) Blocked by required conditions
Tests extras backends / tests-acestep-cpp (push) Blocked by required conditions
Tests extras backends / tests-qwen3-tts-cpp (push) Blocked by required conditions
Tests extras backends / tests-vibevoice-cpp (push) Blocked by required conditions
Tests extras backends / tests-vibevoice-cpp-grpc-tts (push) Blocked by required conditions
Tests extras backends / tests-vibevoice-cpp-grpc-transcription (push) Blocked by required conditions
Tests extras backends / tests-localvqe-grpc-transform (push) Blocked by required conditions
Tests extras backends / tests-voxtral (push) Blocked by required conditions
Tests extras backends / tests-kokoros (push) Blocked by required conditions
Tests extras backends / tests-qwen-tts (push) Blocked by required conditions
Tests extras backends / tests-qwen-asr (push) Blocked by required conditions
Tests extras backends / tests-nemo (push) Blocked by required conditions
Tests extras backends / tests-insightface-grpc (push) Blocked by required conditions
Tests extras backends / tests-speaker-recognition-grpc (push) Blocked by required conditions
tests / tests-linux (1.26.x) (push) Waiting to run
tests / tests-apple (1.26.x) (push) Waiting to run
UI E2E Tests / tests-ui-e2e (1.26.x) (push) Waiting to run
E2E Backend Tests / tests-e2e-backend (1.25.x) (push) Waiting to run
* 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>
138 lines
4.4 KiB
Go
138 lines
4.4 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("BaseURL", func() {
|
|
Context("without prefix", func() {
|
|
It("should return base URL without prefix", func() {
|
|
app := echo.New()
|
|
actualURL := ""
|
|
|
|
// Register route - use the actual request path so routing works
|
|
routePath := "/hello/world"
|
|
app.GET(routePath, func(c echo.Context) error {
|
|
actualURL = BaseURL(c)
|
|
return nil
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/hello/world", nil)
|
|
rec := httptest.NewRecorder()
|
|
app.ServeHTTP(rec, req)
|
|
|
|
Expect(rec.Code).To(Equal(200), "response status code")
|
|
Expect(actualURL).To(Equal("http://example.com/"), "base URL")
|
|
})
|
|
})
|
|
|
|
Context("with prefix", func() {
|
|
It("should return base URL with prefix", func() {
|
|
app := echo.New()
|
|
actualURL := ""
|
|
|
|
// Register route with the stripped path (after middleware removes prefix)
|
|
routePath := "/hello/world"
|
|
app.GET(routePath, func(c echo.Context) error {
|
|
// Simulate what StripPathPrefix middleware does - store original path
|
|
c.Set("_original_path", "/myprefix/hello/world")
|
|
// Modify the request path to simulate prefix stripping
|
|
c.Request().URL.Path = "/hello/world"
|
|
actualURL = BaseURL(c)
|
|
return nil
|
|
})
|
|
|
|
// Make request with stripped path (middleware would have already processed it)
|
|
req := httptest.NewRequest("GET", "/hello/world", nil)
|
|
rec := httptest.NewRecorder()
|
|
app.ServeHTTP(rec, req)
|
|
|
|
Expect(rec.Code).To(Equal(200), "response status code")
|
|
Expect(actualURL).To(Equal("http://example.com/myprefix/"), "base URL")
|
|
})
|
|
})
|
|
|
|
// Caddy's handle_path (and similar reverse-proxy directives) strips the
|
|
// matched prefix before forwarding upstream, so LocalAI receives the
|
|
// already-stripped path together with X-Forwarded-Prefix. In that case
|
|
// StripPathPrefix never stores _original_path, but BaseURL must still
|
|
// honor the header so that <base href> and asset URLs include the prefix.
|
|
Context("with X-Forwarded-Prefix header but pre-stripped path", func() {
|
|
It("should return base URL with prefix from header", func() {
|
|
app := echo.New()
|
|
actualURL := ""
|
|
|
|
routePath := "/app"
|
|
app.GET(routePath, func(c echo.Context) error {
|
|
actualURL = BaseURL(c)
|
|
return nil
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/app", nil)
|
|
req.Header.Set("X-Forwarded-Prefix", "/localai")
|
|
rec := httptest.NewRecorder()
|
|
app.ServeHTTP(rec, req)
|
|
|
|
Expect(rec.Code).To(Equal(200), "response status code")
|
|
Expect(actualURL).To(Equal("http://example.com/localai/"), "base URL")
|
|
})
|
|
|
|
It("should normalize a prefix that already ends with a slash", func() {
|
|
app := echo.New()
|
|
actualURL := ""
|
|
|
|
routePath := "/app"
|
|
app.GET(routePath, func(c echo.Context) error {
|
|
actualURL = BaseURL(c)
|
|
return nil
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/app", nil)
|
|
req.Header.Set("X-Forwarded-Prefix", "/localai/")
|
|
rec := httptest.NewRecorder()
|
|
app.ServeHTTP(rec, req)
|
|
|
|
Expect(rec.Code).To(Equal(200), "response status code")
|
|
Expect(actualURL).To(Equal("http://example.com/localai/"), "base URL")
|
|
})
|
|
})
|
|
|
|
// X-Forwarded-Prefix is attacker controllable on misconfigured proxy
|
|
// chains, and the value flows into the SPA HTML response (<base href>
|
|
// and asset URLs). BasePathPrefix must gate the header through
|
|
// SafeForwardedPrefix so values that turn the prefix into an open
|
|
// redirect or a protocol-relative URL are ignored and the base falls
|
|
// back to "/".
|
|
Context("with unsafe X-Forwarded-Prefix header", func() {
|
|
DescribeTable("falls back to / when the header is unsafe",
|
|
func(header string) {
|
|
app := echo.New()
|
|
actualURL := ""
|
|
|
|
app.GET("/app", func(c echo.Context) error {
|
|
actualURL = BaseURL(c)
|
|
return nil
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/app", nil)
|
|
req.Header.Set("X-Forwarded-Prefix", header)
|
|
rec := httptest.NewRecorder()
|
|
app.ServeHTTP(rec, req)
|
|
|
|
Expect(rec.Code).To(Equal(200), "response status code")
|
|
Expect(actualURL).To(Equal("http://example.com/"), "base URL")
|
|
},
|
|
Entry("protocol-relative URL", "//evil.com"),
|
|
Entry("protocol-relative URL with path", "//evil.com/assets"),
|
|
Entry("backslash path", `/foo\bar`),
|
|
Entry("embedded NUL", "/foo\x00bar"),
|
|
Entry("CR injection", "/foo\rbar"),
|
|
Entry("LF injection", "/foo\nbar"),
|
|
Entry("missing leading slash", "evil"),
|
|
)
|
|
})
|
|
})
|