From a79dc56512352d712ff55c57122731842ab30581 Mon Sep 17 00:00:00 2001 From: Mike Cohen Date: Wed, 30 Oct 2024 16:16:12 +1000 Subject: [PATCH 1/3] Bugfix: Fixed broken support for GUI.base_path Various internal links were broken when a base path was specified. Fixes: #3861 --- api/assets.go | 5 +-- api/authenticators/google.go | 7 ++-- api/authenticators/multiple.go | 4 ++- api/authenticators/template.go | 12 +++++-- .../definitions/Server/Monitor/Health.yaml | 2 +- .../src/components/core/api-service.jsx | 14 ++++---- .../src/components/users/user-label.jsx | 5 +-- .../src/components/welcome/login.jsx | 2 +- .../src/components/welcome/logoff.jsx | 2 +- gui/velociraptor/src/index.html | 3 +- services/notebook/notebook.go | 16 +++++++-- services/sanity/frontend.go | 18 ++++++++++ utils/urls.go | 36 +++++++++++++++++++ vql/server/links.go | 21 ++++++----- 14 files changed, 113 insertions(+), 34 deletions(-) diff --git a/api/assets.go b/api/assets.go index 61620182c03..d1ebbd7eab0 100644 --- a/api/assets.go +++ b/api/assets.go @@ -166,8 +166,9 @@ func NewInterceptingResponseWriter( return &interceptingResponseWriter{ ResponseWriter: w, from: "url(/app/assets/", - to: fmt.Sprintf("url(/%v/app/assets/", base_path), - br_writer: brotli.NewWriter(w), + to: fmt.Sprintf("url(/%v/app/assets/", + strings.TrimPrefix(base_path, "/")), + br_writer: brotli.NewWriter(w), } } } diff --git a/api/authenticators/google.go b/api/authenticators/google.go index b0239cc346d..ef23d2e7dbb 100644 --- a/api/authenticators/google.go +++ b/api/authenticators/google.go @@ -20,7 +20,6 @@ package authenticators import ( "crypto/rand" "encoding/base64" - "errors" "fmt" "io" "io/ioutil" @@ -290,14 +289,14 @@ func authenticateUserHandle( users := services.GetUserManager() user_record, err := users.GetUser(r.Context(), username, username) if err != nil { - reject_cb(w, r, errors.New("Invalid user"), username) + reject_cb(w, r, fmt.Errorf("Invalid user: %v", err), username) return } // Does the user have access to the specified org? err = CheckOrgAccess(config_obj, r, user_record) if err != nil { - reject_cb(w, r, errors.New("Insufficient permissions"), user_record.Name) + reject_cb(w, r, fmt.Errorf("Insufficient permissions: %v", err), user_record.Name) return } @@ -349,7 +348,7 @@ func reject_with_username( w.WriteHeader(http.StatusOK) renderRejectionMessage(config_obj, - w, username, []velociraptor.AuthenticatorInfo{ + r, w, err, username, []velociraptor.AuthenticatorInfo{ { LoginURL: login_url, ProviderName: provider, diff --git a/api/authenticators/multiple.go b/api/authenticators/multiple.go index db988966456..be8d6031353 100644 --- a/api/authenticators/multiple.go +++ b/api/authenticators/multiple.go @@ -47,7 +47,9 @@ func (self *MultiAuthenticator) reject_with_username( w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusUnauthorized) - renderRejectionMessage(self.config_obj, w, username, self.delegate_info) + renderRejectionMessage( + self.config_obj, r, w, err, + username, self.delegate_info) } func (self *MultiAuthenticator) AuthenticateUserHandler( diff --git a/api/authenticators/template.go b/api/authenticators/template.go index 0fe36f2b76c..9ad8bfee001 100644 --- a/api/authenticators/template.go +++ b/api/authenticators/template.go @@ -2,6 +2,7 @@ package authenticators import ( "net/http" + "strings" "text/template" utils "www.velocidex.com/golang/velociraptor/api/utils" @@ -13,8 +14,15 @@ import ( func renderRejectionMessage( config_obj *config_proto.Config, - w http.ResponseWriter, username string, - authenticators []velociraptor.AuthenticatorInfo) { + r *http.Request, w http.ResponseWriter, err error, + username string, authenticators []velociraptor.AuthenticatorInfo) { + + // For API calls we render the error as JSON + base_path := config_obj.GUI.BasePath + "/api/" + if strings.HasPrefix(r.URL.Path, base_path) { + w.Write([]byte(json.Format(`{"message": %q}`, err.Error()))) + return + } data, err := gui_assets.ReadFile("/index.html") if err != nil { diff --git a/artifacts/definitions/Server/Monitor/Health.yaml b/artifacts/definitions/Server/Monitor/Health.yaml index 44763453e79..b495107a498 100644 --- a/artifacts/definitions/Server/Monitor/Health.yaml +++ b/artifacts/definitions/Server/Monitor/Health.yaml @@ -86,7 +86,7 @@ reports: ## Current Orgs {{ define "OrgsTable" }} - LET ColumnTypes <= dict(ClientConfig='url_internal') + LET ColumnTypes <= dict(ClientConfig='url') LET OrgsTable = SELECT Name, OrgId, upload(accessor='data', file=_client_config, name='client.'+OrgId+'.config.yaml') AS _Upload diff --git a/gui/velociraptor/src/components/core/api-service.jsx b/gui/velociraptor/src/components/core/api-service.jsx index c1368cd1d4b..36d03393b55 100644 --- a/gui/velociraptor/src/components/core/api-service.jsx +++ b/gui/velociraptor/src/components/core/api-service.jsx @@ -260,7 +260,7 @@ const upload = function(url, files, params) { }; // Internal Routes declared in api/proxy.go -const internal_links = new RegExp("^/notebooks|downloads|hunts|clients/"); +const internal_links = new RegExp("^/app|notebooks|downloads|hunts|clients/"); const api_links = new RegExp("^/api/"); // Prepare a suitable href link for @@ -290,13 +290,11 @@ const href = function(url, params, options) { // All internal links must point to the same page since this // is a SPA - if (options && options.internal) { - if (internal_links.test(parsed.pathname) || - api_links.test(parsed.pathname)) { - parsed.pathname = src_of(parsed.pathname); - } else { - parsed.pathname = window.location.pathname; - } + if (internal_links.test(parsed.pathname) || + api_links.test(parsed.pathname)) { + parsed.pathname = src_of(parsed.pathname); + } else { + parsed.pathname = window.location.pathname; } } diff --git a/gui/velociraptor/src/components/users/user-label.jsx b/gui/velociraptor/src/components/users/user-label.jsx index dc679fd1b45..7d232d5e29e 100644 --- a/gui/velociraptor/src/components/users/user-label.jsx +++ b/gui/velociraptor/src/components/users/user-label.jsx @@ -476,8 +476,9 @@ export default class UserLabel extends React.Component { api.post("v1/SetGUIOptions", params, this.source.token).then((response) => { if (response.status === 200) { - // Check for redirect from the server - this is normally - // set by the authenticator to redirect to a better server. + // Check for redirect from the server - this + // is normally set by the authenticator to + // redirect to a better server. if (response.data && response.data.redirect_url && response.data.redirect_url !== "") { window.location.assign(response.data.redirect_url); diff --git a/gui/velociraptor/src/components/welcome/login.jsx b/gui/velociraptor/src/components/welcome/login.jsx index 85aef24fedd..2b8932519fb 100644 --- a/gui/velociraptor/src/components/welcome/login.jsx +++ b/gui/velociraptor/src/components/welcome/login.jsx @@ -79,7 +79,7 @@ export default class LoginPage extends Component { enforceFocus={true} scrollable={true} onHide={() => { - window.location = '/'; + window.location = api.href("/app/index.html"); }}> Velociraptor Login diff --git a/gui/velociraptor/src/components/welcome/logoff.jsx b/gui/velociraptor/src/components/welcome/logoff.jsx index 21ed6891004..928808bbb1f 100644 --- a/gui/velociraptor/src/components/welcome/logoff.jsx +++ b/gui/velociraptor/src/components/welcome/logoff.jsx @@ -21,7 +21,7 @@ export default class LogoffPage extends Component { enforceFocus={true} scrollable={true} onHide={() => { - window.location = '/'; + window.location = api.href("/app/index.html"); }}> Velociraptor Login diff --git a/gui/velociraptor/src/index.html b/gui/velociraptor/src/index.html index 9976204e1a7..037c86ac96f 100644 --- a/gui/velociraptor/src/index.html +++ b/gui/velociraptor/src/index.html @@ -14,7 +14,8 @@ window.base_path = "{{.BasePath}}"; /// Support development if (window.base_path.substring(0,2) === "\{\{") { - window.base_path = ""; + window.base_path = "/"; + window.globals.base_path = window.base_path; } // Set the OrgId from the URL if possible. diff --git a/services/notebook/notebook.go b/services/notebook/notebook.go index 725e8020967..43c13b1e4ff 100644 --- a/services/notebook/notebook.go +++ b/services/notebook/notebook.go @@ -4,8 +4,8 @@ import ( "context" "encoding/base64" "errors" - "net/url" "os" + "path" "strings" "sync" @@ -186,9 +186,19 @@ func (self *NotebookManager) UploadNotebookAttachment( return nil, err } + public_url, err := utils.GetBaseURL(self.config_obj) + if err != nil { + return nil, err + } + + // Calculate the URL to the resource + public_url.Path = path.Join(public_url.Path, full_path.AsClientPath()) + values := public_url.Query() + values.Set("org_id", utils.NormalizedOrgId(self.config_obj.OrgId)) + public_url.RawQuery = values.Encode() + result := &api_proto.NotebookFileUploadResponse{ - Url: full_path.AsClientPath() + "?org_id=" + - url.QueryEscape(utils.NormalizedOrgId(self.config_obj.OrgId)), + Url: public_url.String(), Filename: filename, } diff --git a/services/sanity/frontend.go b/services/sanity/frontend.go index 81f4a506ef8..e0841a2cca6 100644 --- a/services/sanity/frontend.go +++ b/services/sanity/frontend.go @@ -3,11 +3,17 @@ package sanity import ( "fmt" "net" + "regexp" + "strings" config_proto "www.velocidex.com/golang/velociraptor/config/proto" "www.velocidex.com/golang/velociraptor/logging" ) +var ( + basePathRegEx = regexp.MustCompile("^/[^/][a-zA-Z0-9]+[^/]$") +) + func (self *SanityChecks) CheckFrontendSettings( config_obj *config_proto.Config) error { @@ -22,6 +28,18 @@ func (self *SanityChecks) CheckFrontendSettings( } logger.Info("GUI Will only accept conections from %v", cidr_net) } + + if config_obj.GUI.BasePath != "" { + if !basePathRegEx.MatchString(config_obj.GUI.BasePath) { + return fmt.Errorf("Invalid GUI.base_path. This must start with a / and end without a /. For example '/velociraptor' . Only a-z0-9 characters are allowed in the path name.") + } + + if !strings.HasSuffix(config_obj.GUI.PublicUrl, + config_obj.GUI.BasePath+"/app/index.html") { + return fmt.Errorf("Invalid GUI.public_url. When setting a base_url the public_url must be adjusted accordingly. For example `https://www.example.com/velociraptor/app/index.html` for a base_path of `/velociraptor` .") + } + } + } return nil } diff --git a/utils/urls.go b/utils/urls.go index 957cdcb6c85..97ee80a1a2b 100644 --- a/utils/urls.go +++ b/utils/urls.go @@ -1,8 +1,12 @@ package utils import ( + "fmt" "net/url" + "path" "strings" + + config_proto "www.velocidex.com/golang/velociraptor/config/proto" ) // Work around issues with https://github.com/golang/go/issues/4013 @@ -12,3 +16,35 @@ func QueryEscape(in string) string { res := url.QueryEscape(in) return strings.Replace(res, "+", "%20", -1) } + +// The URL to the App.html itself +func GetPublicUrl(config_obj *config_proto.Config) (res *url.URL, err error) { + res = &url.URL{Path: "/"} + + if config_obj.GUI != nil && config_obj.GUI.PublicUrl != "" { + res, err = url.Parse(config_obj.GUI.PublicUrl) + if err != nil { + return nil, fmt.Errorf( + "Invalid configuration! GUI.public_url must be the public URL over which the GUI is served!: %w", err) + } + } + res.RawQuery = "" + res.Fragment = "" + res.RawFragment = "" + + return res, nil +} + +// Calculates the Base URL to the top of the app +func GetBaseURL(config_obj *config_proto.Config) (res *url.URL, err error) { + res, err = GetPublicUrl(config_obj) + if err != nil { + return nil, err + } + res.Path = "/" + if config_obj.GUI != nil && config_obj.GUI.BasePath != "" { + res.Path = path.Join(res.Path, config_obj.GUI.BasePath) + } + + return res, nil +} diff --git a/vql/server/links.go b/vql/server/links.go index a75f70b9a35..2a46c6660bc 100644 --- a/vql/server/links.go +++ b/vql/server/links.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "net/url" + "path" "strings" "github.com/Velocidex/ordereddict" "www.velocidex.com/golang/velociraptor/services" + "www.velocidex.com/golang/velociraptor/utils" "www.velocidex.com/golang/velociraptor/vql" vql_subsystem "www.velocidex.com/golang/velociraptor/vql" "www.velocidex.com/golang/vfilter" @@ -48,13 +50,10 @@ func (self *LinkToFunction) Call(ctx context.Context, return vfilter.Null{} } - url := &url.URL{Path: "/"} - if config_obj.GUI.PublicUrl != "" { - url, err = url.Parse(config_obj.GUI.PublicUrl) - if err != nil { - scope.Log("link_to: Invalid configuration! GUI.public_url must be the public URL over which the GUI is served!: %v", err) - return vfilter.Null{} - } + url, err := utils.GetPublicUrl(config_obj) + if err != nil { + scope.Log("link_to: %v", err) + return vfilter.Null{} } org := arg.OrgId @@ -98,7 +97,13 @@ func (self *LinkToFunction) Call(ctx context.Context, } // The link is an API call to VFSDownloadInfo - url.Path = "/api/v1/DownloadVFSFile" + url, err = utils.GetBaseURL(config_obj) + if err != nil { + scope.Log("link_to: %v", err) + return vfilter.Null{} + } + + url.Path = path.Join(url.Path, "/api/v1/DownloadVFSFile") query.Add("vfs_path", vfs_name) for _, c := range components { query.Add("fs_components", c) From 675cde80a94501c80f1dea13781d1aaeee83c107 Mon Sep 17 00:00:00 2001 From: Mike Cohen Date: Wed, 30 Oct 2024 16:41:05 +1000 Subject: [PATCH 2/3] Handle better path concatenation --- gui/velociraptor/package-lock.json | 11 +++++++++++ gui/velociraptor/package.json | 1 + .../src/components/core/api-service.jsx | 14 ++++++++------ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/gui/velociraptor/package-lock.json b/gui/velociraptor/package-lock.json index 6dff92dfb4f..6e9a489f406 100644 --- a/gui/velociraptor/package-lock.json +++ b/gui/velociraptor/package-lock.json @@ -33,6 +33,7 @@ "moment-timezone": "0.5.45", "npm-watch": "^0.13.0", "patch-package": "8.0.0", + "path-browserify": "1.0.1", "prop-types": "^15.8.1", "qs": "6.13.0", "react": "^16.14.0", @@ -7534,6 +7535,11 @@ "node": ">= 14" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -14908,6 +14914,11 @@ } } }, + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", diff --git a/gui/velociraptor/package.json b/gui/velociraptor/package.json index 55cbe8b106b..109141ba208 100644 --- a/gui/velociraptor/package.json +++ b/gui/velociraptor/package.json @@ -28,6 +28,7 @@ "moment-timezone": "0.5.45", "npm-watch": "^0.13.0", "patch-package": "8.0.0", + "path-browserify": "1.0.1", "prop-types": "^15.8.1", "qs": "6.13.0", "react": "^16.14.0", diff --git a/gui/velociraptor/src/components/core/api-service.jsx b/gui/velociraptor/src/components/core/api-service.jsx index 36d03393b55..e30b2871f9a 100644 --- a/gui/velociraptor/src/components/core/api-service.jsx +++ b/gui/velociraptor/src/components/core/api-service.jsx @@ -1,4 +1,5 @@ import axios, {isCancel} from 'axios'; +import path from 'path-browserify'; import _ from 'lodash'; import qs from 'qs'; @@ -259,9 +260,10 @@ const upload = function(url, files, params) { }).catch(handle_error); }; -// Internal Routes declared in api/proxy.go -const internal_links = new RegExp("^/app|notebooks|downloads|hunts|clients/"); -const api_links = new RegExp("^/api/"); +// Internal Routes declared in api/proxy.go Assume base_path is regex +// safe due to the sanitation in the sanitation service. +const internal_links = new RegExp( + "^" + base_path + "/api|app|notebooks|downloads|hunts|clients/"); // Prepare a suitable href link for // This function accepts a number of options: @@ -290,8 +292,7 @@ const href = function(url, params, options) { // All internal links must point to the same page since this // is a SPA - if (internal_links.test(parsed.pathname) || - api_links.test(parsed.pathname)) { + if (internal_links.test(parsed.pathname)) { parsed.pathname = src_of(parsed.pathname); } else { parsed.pathname = window.location.pathname; @@ -328,11 +329,12 @@ const delete_req = function(url, params, cancel_token) { var hooks = []; +// Returns a url corrected for base_path. Handles data URLs properly. const src_of = function (url) { if (url && url.match(/^data/)) { return url; } - return window.base_path + url; + return path.join(window.base_path, url); }; const error = function(msg) { From 3ca3519d109c31580ce16ce171eb9627681807de Mon Sep 17 00:00:00 2001 From: Mike Cohen Date: Wed, 30 Oct 2024 16:49:56 +1000 Subject: [PATCH 3/3] Allow extra chars in base path --- services/sanity/frontend.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sanity/frontend.go b/services/sanity/frontend.go index e0841a2cca6..763727d5e03 100644 --- a/services/sanity/frontend.go +++ b/services/sanity/frontend.go @@ -11,7 +11,7 @@ import ( ) var ( - basePathRegEx = regexp.MustCompile("^/[^/][a-zA-Z0-9]+[^/]$") + basePathRegEx = regexp.MustCompile("^/[^/][a-zA-Z0-9_-]+[^/]$") ) func (self *SanityChecks) CheckFrontendSettings(