mirror of
https://github.com/rclone/rclone.git
synced 2026-10-28 14:28:38 -04:00
rest: add URLJoinRoot to join a path onto a URL without changing its host GHSA-xqmq-v4wj-rw5m CVE-PENDING
rest.URLJoin resolves its path as a URL reference according to the URL standard, so a path beginning with `//` is a network-path reference which replaces the host (and port and userinfo) of the base URL. That is often the wrong behaviour. Add rest.URLJoinRoot which joins an unescaped root onto a base URL and refuses any root which parses with a scheme, host or userinfo of its own.
This commit is contained in:
1 parent
25707f2dc6
commit
28562c8a79
2 files changed
+60
No files matched your search
@@ -9,6 +9,10 @@ import (
|
||||
// URLJoin joins a URL and a path returning a new URL
|
||||
//
|
||||
// path should be URL escaped
|
||||
//
|
||||
// path is resolved as a URL reference so an absolute URL or a
|
||||
// network-path reference ("//host/path") replaces the scheme or host
|
||||
// of base. Use URLJoinRoot for a path which must stay on base.
|
||||
func URLJoin(base *url.URL, path string) (*url.URL, error) {
|
||||
rel, err := url.Parse(path)
|
||||
if err != nil {
|
||||
@@ -17,6 +21,22 @@ func URLJoin(base *url.URL, path string) (*url.URL, error) {
|
||||
return base.ResolveReference(rel), nil
|
||||
}
|
||||
|
||||
// URLJoinRoot joins root, an unescaped path, onto base returning a new URL.
|
||||
//
|
||||
// root may only change the path of base. A root which carries a
|
||||
// scheme, host or userinfo of its own, such as the network-path
|
||||
// reference "//host/path", returns an error.
|
||||
func URLJoinRoot(base *url.URL, root string) (*url.URL, error) {
|
||||
rel, err := url.Parse(URLPathEscape(root))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing %q as URL: %w", root, err)
|
||||
}
|
||||
if rel.Scheme != "" || rel.Opaque != "" || rel.User != nil || rel.Host != "" {
|
||||
return nil, fmt.Errorf("root %q must not change the scheme, host or user of %q", root, base.Redacted())
|
||||
}
|
||||
return base.ResolveReference(rel), nil
|
||||
}
|
||||
|
||||
// URLPathEscape escapes URL path the in string using URL escaping rules
|
||||
//
|
||||
// This mimics url.PathEscape which only available from go 1.8
|
||||
|
||||
@@ -44,6 +44,46 @@ func TestURLJoin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLJoinRoot(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
base string
|
||||
root string
|
||||
wantOK bool
|
||||
want string
|
||||
}{
|
||||
{"http://example.com/dir/", "", true, "http://example.com/dir/"},
|
||||
{"http://example.com/dir/", "potato", true, "http://example.com/dir/potato"},
|
||||
{"http://example.com/dir/", "sub/potato/", true, "http://example.com/dir/sub/potato/"},
|
||||
{"http://example.com/dir/", "/potato", true, "http://example.com/potato"},
|
||||
{"http://example.com/dir/", "../potato", true, "http://example.com/potato"},
|
||||
{"http://example.com/dir/", "With colon :", true, "http://example.com/dir/With%20colon%20:"},
|
||||
{"http://example.com/dir/", "http://example.com/dir/", true, "http://example.com/dir/http://example.com/dir/"},
|
||||
{"http://user:pass@example.com/dir/", "potato", true, "http://user:pass@example.com/dir/potato"},
|
||||
// network-path references are rejected, even for the same host
|
||||
{"http://example.com/dir/", "//evil.example/", false, ""},
|
||||
{"http://example.com/dir/", "//evil.example", false, ""},
|
||||
{"http://example.com/dir/", "//evil.example/potato", false, ""},
|
||||
{"http://example.com/dir/", "//example.com/dir/", false, ""},
|
||||
{"http://example.com/dir/", "//example.com:8080/dir/", false, ""},
|
||||
{"http://example.com/dir/", "//user:pass@example.com/dir/", false, ""},
|
||||
{"http://user:pass@example.com/dir/", "//other:pass@example.com/dir/", false, ""},
|
||||
{"http://user:pass@example.com/dir/", "//example.com/dir/", false, ""},
|
||||
{"http://example.com/dir/", "//[::1]/potato", false, ""},
|
||||
} {
|
||||
u, err := url.Parse(test.base)
|
||||
require.NoError(t, err)
|
||||
got, err := URLJoinRoot(u, test.root)
|
||||
gotOK := err == nil
|
||||
what := fmt.Sprintf("test %d base=%q, root=%q", i, test.base, test.root)
|
||||
assert.Equal(t, test.wantOK, gotOK, what)
|
||||
var gotString string
|
||||
if gotOK {
|
||||
gotString = got.String()
|
||||
}
|
||||
assert.Equal(t, test.want, gotString, what)
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLPathEscape(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
path string
|
||||
|
||||
Reference in new issue
Block a user