Skip to content

Commit

Permalink
Bugfix: Fixed broken support for GUI.base_path (#3862)
Browse files Browse the repository at this point in the history
Various internal links were broken when a base path was specified.

Fixes: #3861
  • Loading branch information
scudette authored Oct 30, 2024
1 parent f79e682 commit 9dcc9a6
Show file tree
Hide file tree
Showing 16 changed files with 130 additions and 37 deletions.
5 changes: 3 additions & 2 deletions api/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
}
Expand Down
7 changes: 3 additions & 4 deletions api/authenticators/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package authenticators
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion api/authenticators/multiple.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
12 changes: 10 additions & 2 deletions api/authenticators/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package authenticators

import (
"net/http"
"strings"
"text/template"

utils "www.velocidex.com/golang/velociraptor/api/utils"
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion artifacts/definitions/Server/Monitor/Health.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions gui/velociraptor/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gui/velociraptor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 11 additions & 11 deletions gui/velociraptor/src/components/core/api-service.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios, {isCancel} from 'axios';
import path from 'path-browserify';

import _ from 'lodash';
import qs from 'qs';
Expand Down Expand Up @@ -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("^/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 <a>
// This function accepts a number of options:
Expand Down Expand Up @@ -290,13 +292,10 @@ 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)) {
parsed.pathname = src_of(parsed.pathname);
} else {
parsed.pathname = window.location.pathname;
}
}

Expand Down Expand Up @@ -330,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) {
Expand Down
5 changes: 3 additions & 2 deletions gui/velociraptor/src/components/users/user-label.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion gui/velociraptor/src/components/welcome/login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default class LoginPage extends Component {
enforceFocus={true}
scrollable={true}
onHide={() => {
window.location = '/';
window.location = api.href("/app/index.html");
}}>
<Modal.Header closeButton>
<Modal.Title>Velociraptor Login</Modal.Title>
Expand Down
2 changes: 1 addition & 1 deletion gui/velociraptor/src/components/welcome/logoff.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class LogoffPage extends Component {
enforceFocus={true}
scrollable={true}
onHide={() => {
window.location = '/';
window.location = api.href("/app/index.html");
}}>
<Modal.Header closeButton>
<Modal.Title>Velociraptor Login</Modal.Title>
Expand Down
3 changes: 2 additions & 1 deletion gui/velociraptor/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 13 additions & 3 deletions services/notebook/notebook.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"context"
"encoding/base64"
"errors"
"net/url"
"os"
"path"
"strings"
"sync"

Expand Down Expand Up @@ -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,
}

Expand Down
18 changes: 18 additions & 0 deletions services/sanity/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -22,6 +28,18 @@ func (self *SanityChecks) CheckFrontendSettings(
}
logger.Info("GUI Will only accept conections from <green>%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
}
36 changes: 36 additions & 0 deletions utils/urls.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}
21 changes: 13 additions & 8 deletions vql/server/links.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 9dcc9a6

Please sign in to comment.