hub: return 404 status for unknown frontend routes (#2414)

This commit is contained in:
henrygd
2026-09-26 10:17:25 -04:00
parent 24ec18a937
commit 15994474f6
3 changed files with 93 additions and 1 deletions
+26
View File
@@ -28,6 +28,32 @@ func modifyIndexHTML(hub *Hub, html []byte) string {
return strings.Replace(htmlContent, "\"{info}\"", string(content), 1)
}
// isAppRoute reports whether urlPath matches a frontend route, so unknown paths
// can be served with a 404 status. The base path prefix is optional because
// reverse proxies may or may not strip it before forwarding.
//
// Keep in sync with routes in internal/site/src/components/router.tsx.
func isAppRoute(urlPath, basePath string) bool {
urlPath = strings.ToLower(urlPath)
if base := strings.TrimSuffix(strings.ToLower(basePath), "/"); base != "" {
if rest, ok := strings.CutPrefix(urlPath, base); ok && (rest == "" || rest[0] == '/') {
urlPath = rest
}
}
urlPath = strings.TrimSuffix(urlPath, "/")
switch urlPath {
case "", "/containers", "/smart", "/monitors", "/settings", "/forgot-password", "/request-otp":
return true
}
// routes with a single required (/system/:id) or optional (/settings/:name?) param
for _, prefix := range [...]string{"/system/", "/settings/"} {
if param, ok := strings.CutPrefix(urlPath, prefix); ok {
return param != "" && !strings.Contains(param, "/")
}
}
return false
}
func getPublicAppInfo(hub *Hub) PublicAppInfo {
parsedURL, _ := url.Parse(hub.appURL)
info := PublicAppInfo{
+8 -1
View File
@@ -18,6 +18,7 @@ import (
func (h *Hub) startServer(se *core.ServeEvent) error {
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
html := modifyIndexHTML(h, indexFile)
basePath := getPublicAppInfo(h).BASE_PATH
// set up static asset serving
staticPaths := [2]string{"/static/", "/assets/"}
serveStatic := apis.Static(site.DistDirFS, false)
@@ -36,7 +37,13 @@ func (h *Hub) startServer(se *core.ServeEvent) error {
e.Response.Header().Del("X-Frame-Options")
e.Response.Header().Set("Content-Security-Policy", csp)
}
return e.HTML(http.StatusOK, html)
// still serve the app for unknown paths (it renders a 404 page),
// but with a 404 status so scanners and fail2ban see the miss
status := http.StatusOK
if !isAppRoute(e.Request.URL.Path, basePath) {
status = http.StatusNotFound
}
return e.HTML(status, html)
})
return nil
}
+59
View File
@@ -0,0 +1,59 @@
//go:build testing
package hub
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsAppRoute(t *testing.T) {
tests := []struct {
path string
basePath string
want bool
}{
// known routes
{"/", "/", true},
{"/containers", "/", true},
{"/containers/", "/", true},
{"/Containers", "/", true},
{"/smart", "/", true},
{"/monitors", "/", true},
{"/forgot-password", "/", true},
{"/request-otp", "/", true},
{"/system/abc123", "/", true},
{"/system/abc123/", "/", true},
{"/settings", "/", true},
{"/settings/general", "/", true},
// unknown paths
{"/.env", "/", false},
{"/phpinfo.php", "/", false},
{"/wp-admin/", "/", false},
{"/.git/config", "/", false},
{"/system", "/", false},
{"/system/", "/", false},
{"/system/abc/def", "/", false},
{"/settings/general/extra", "/", false},
{"/containersx", "/", false},
// base path, prefix not stripped by proxy
{"/beszel", "/beszel/", true},
{"/beszel/", "/beszel/", true},
{"/beszel/containers", "/beszel/", true},
{"/beszel/system/abc123", "/beszel/", true},
{"/beszel/.env", "/beszel/", false},
{"/beszelx", "/beszel/", false},
// base path, prefix stripped by proxy
{"/", "/beszel/", true},
{"/containers", "/beszel/", true},
{"/.env", "/beszel/", false},
}
for _, tt := range tests {
assert.Equal(t, tt.want, isAppRoute(tt.path, tt.basePath), "path=%q basePath=%q", tt.path, tt.basePath)
}
}