ipn/ipnlocal: update getServeHandler path handling on malformed url (#20431)
This commit updates the path matching logic in getServeHandler for malformed request targets like "*" (e.g. "GET *") and "" (e.g. "CONNECT" authority-form). Those paths never reduce to "/" as absolute path would. An absolute path check was added and an additional check on no further reduce was added in the loop. Fixes tailscale/corp#44814 Signed-off-by: kevinliang10 <kevinliang@tailscale.com>
This commit is contained in:
+16
-1
@@ -818,6 +818,15 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
|
|||||||
return h, r.URL.Path, true
|
return h, r.URL.Path, true
|
||||||
}
|
}
|
||||||
pth := path.Clean(r.URL.Path)
|
pth := path.Clean(r.URL.Path)
|
||||||
|
// A well-formed origin-form request path is absolute. Malformed request
|
||||||
|
// targets — "*" (e.g. "GET *") and "" (e.g. "CONNECT" authority-form),
|
||||||
|
// clean to "*" and "." respectively. Those are path.Dir fixed points that
|
||||||
|
// never equal "/" and match no mount, so without this guard the loop below
|
||||||
|
// would spin forever on one CPU core (a remote DoS via serve, or via funnel
|
||||||
|
// from the internet).
|
||||||
|
if !strings.HasPrefix(pth, "/") {
|
||||||
|
return z, "", false
|
||||||
|
}
|
||||||
for {
|
for {
|
||||||
withSlash := pth + "/"
|
withSlash := pth + "/"
|
||||||
if h, ok := wsc.Handlers().GetOk(withSlash); ok {
|
if h, ok := wsc.Handlers().GetOk(withSlash); ok {
|
||||||
@@ -829,7 +838,13 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
|
|||||||
if pth == "/" {
|
if pth == "/" {
|
||||||
return z, "", false
|
return z, "", false
|
||||||
}
|
}
|
||||||
pth = path.Dir(pth)
|
// Belt-and-suspenders with the absolute-path check above: stop if
|
||||||
|
// path.Dir stops shrinking rather than assuming it always reaches "/".
|
||||||
|
if parent := path.Dir(pth); parent != pth {
|
||||||
|
pth = parent
|
||||||
|
} else {
|
||||||
|
return z, "", false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,34 @@ func TestGetServeHandler(t *testing.T) {
|
|||||||
path: "/foo/../../../../../../../../etc/passwd",
|
path: "/foo/../../../../../../../../etc/passwd",
|
||||||
want: "/",
|
want: "/",
|
||||||
},
|
},
|
||||||
|
// Malformed request targets that net/http hands the handler verbatim.
|
||||||
|
// These clean to a path.Dir fixed point ("*" or ".") that never reaches
|
||||||
|
// "/", and once spun the getServeHandler loop below forever (a remote
|
||||||
|
// serve/funnel DoS). They must resolve to not-found, not hang.
|
||||||
|
{
|
||||||
|
name: "asterisk", // "GET *"
|
||||||
|
conf: conf1,
|
||||||
|
path: "*",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty", // "CONNECT" authority-form sets URL.Path to ""
|
||||||
|
conf: conf1,
|
||||||
|
path: "",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dot",
|
||||||
|
conf: conf1,
|
||||||
|
path: ".",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "asterisk-subpath",
|
||||||
|
conf: conf1,
|
||||||
|
path: "*/foo",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@@ -209,6 +237,9 @@ func TestGetServeHandler(t *testing.T) {
|
|||||||
DestPort: port,
|
DestPort: port,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// A malformed target like "*" or "" once spun getServeHandler's
|
||||||
|
// path-walk loop forever; a regression would hang here until the
|
||||||
|
// package test timeout fires.
|
||||||
h, got, ok := b.getServeHandler(req)
|
h, got, ok := b.getServeHandler(req)
|
||||||
if (got != "") != ok {
|
if (got != "") != ok {
|
||||||
t.Fatalf("got ok=%v, but got mountPoint=%q", ok, got)
|
t.Fatalf("got ok=%v, but got mountPoint=%q", ok, got)
|
||||||
|
|||||||
Reference in New Issue
Block a user