Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## v1.12.2
### Fixed
* Fixes a bug where `fs.Chmod` would change the symlink target possibly allowing a malicious user to modify files outside their home directory.
* Improved error handling when downloading files to not log as a 500-level error, preferring a 400-level response.
* Fixes JWT verification logic to confirm that the token has the required scopes for the target subsystem.

## v1.12.1
### Added
* Add mount for /etc/machine-id for servers for Hytale ([#292](https://github.com/pterodactyl/wings/pull/292))
Expand Down
27 changes: 26 additions & 1 deletion environment/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package environment
import (
"fmt"
"math"
"os"
"strconv"

"github.com/apex/log"
Expand Down Expand Up @@ -107,11 +108,15 @@ func (l Limits) AsContainerResources() container.Resources {
Memory: l.BoundedMemoryLimit(),
MemoryReservation: l.MemoryLimit * 1024 * 1024,
MemorySwap: l.ConvertedSwap(),
BlkioWeight: l.IoWeight,
OomKillDisable: &l.OOMDisabled,
PidsLimit: &pids,
}

// Only set the block IO weight when the host's cgroup hierarchy can honor it.
if blkioWeightSupported() {
resources.BlkioWeight = l.IoWeight
}

// If the CPU Limit is not set, don't send any of these fields through. Providing
// them seems to break some Java services that try to read the available processors.
//
Expand All @@ -131,6 +136,26 @@ func (l Limits) AsContainerResources() container.Resources {
return resources
}

// blkioWeightSupported reports whether the host's cgroup hierarchy can honor a
// container block IO weight. On cgroup v2 the io.weight knob must be present or
// runc fails container creation; cgroup v1/hybrid always supports it.
func blkioWeightSupported() bool {
// cgroup v1/hybrid honors the weight via blkio.weight; only v2 needs probing.
if _, err := os.Stat("/sys/fs/cgroup/cgroup.controllers"); err != nil {
return true
}
// On v2 the knob lives on the delegated child cgroups, not the root.
for _, p := range []string{
"/sys/fs/cgroup/system.slice/io.weight",
"/sys/fs/cgroup/io.weight",
} {
if _, err := os.Stat(p); err == nil {
return true
}
}
return false
}

type Variables map[string]interface{}

// Get is an ugly hacky function to handle environment variables that get passed
Expand Down
2 changes: 1 addition & 1 deletion internal/ufs/fs_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func (fs *UnixFS) Chmodat(dirfd int, name string, mode FileMode) error {
}

func (fs *UnixFS) fchmodat(op string, dirfd int, name string, mode FileMode) error {
return ensurePathError(unix.Fchmodat(dirfd, name, uint32(mode), 0), op, name)
return ensurePathError(unix.Fchmodat(dirfd, name, uint32(mode), AT_SYMLINK_NOFOLLOW), op, name)
}

// Chown changes the numeric uid and gid of the named file.
Expand Down
11 changes: 8 additions & 3 deletions parser/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac

// Look for the key in the configuration file, and if found return that value to the
// calling function.
match, _, _, err := jsonparser.Get(f.configuration, path...)
match, dataType, _, err := jsonparser.Get(f.configuration, path...)
if err != nil {
if err != jsonparser.KeyPathNotFoundError {
return string(match), err
Expand All @@ -248,7 +248,12 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac
// If there is no key, keep the original value intact, that way it is obvious there
// is a replace issue at play.
return string(match), nil
} else {
return configMatchRegex.ReplaceAllString(cfr.ReplaceWith.String(), string(match)), nil
}

// Only substitute scalar values, not whole objects or arrays.
if dataType == jsonparser.Object || dataType == jsonparser.Array {
return cfr.ReplaceWith.String(), nil
}

return configMatchRegex.ReplaceAllString(cfr.ReplaceWith.String(), string(match)), nil
}
19 changes: 17 additions & 2 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,28 @@ func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error {
return nil
}

type templatableConfig struct {
Docker struct {
Interface string `json:"interface"`
Network struct {
Interface string `json:"interface"`
} `json:"network"`
} `json:"docker"`
}

func newTemplatableConfig(c *config.Configuration) templatableConfig {
var t templatableConfig
t.Docker.Interface = c.Docker.Network.Interface
t.Docker.Network.Interface = c.Docker.Network.Interface
return t
}

// Parse parses a given configuration file and updates all the values within
// as defined in the API response from the Panel.
func (f *ConfigurationFile) Parse(file ufs.File) error {
// log.WithField("path", path).WithField("parser", f.Parser.String()).Debug("parsing server configuration file")

// What the fuck is going on here?
if mb, err := json.Marshal(config.Get()); err != nil {
if mb, err := json.Marshal(newTemplatableConfig(config.Get())); err != nil {
return err
} else {
f.configuration = mb
Expand Down
6 changes: 5 additions & 1 deletion router/router.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package router

import (
"regexp"

"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
Expand All @@ -11,6 +13,8 @@ import (
wserver "github.com/pterodactyl/wings/server"
)

var tokenRegex = regexp.MustCompile(`([?|&]token=)([^&]+)($|&)`)

// Configure configures the routing infrastructure for this daemon instance.
func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
gin.SetMode("release")
Expand All @@ -34,7 +38,7 @@ func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
"status": params.StatusCode,
"latency": params.Latency,
"request_id": params.Keys["request_id"],
}).Debugf("%s %s", params.MethodColor()+params.Method+params.ResetColor(), params.Path)
}).Debugf("%s %s", params.Method, tokenRegex.ReplaceAllString(params.Path, "$1***$3"))

return ""
}))
Expand Down
4 changes: 2 additions & 2 deletions router/router_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func getDownloadBackup(c *gin.Context) {
}

// Get the server using the UUID from the token.
if _, ok := manager.Get(token.ServerUuid); !ok || !token.IsUniqueRequest() {
if _, ok := manager.Get(token.ServerUuid); !ok || !token.IsUniqueRequest() || !token.HasScope(tokens.BackupDownload) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on this server.",
})
Expand Down Expand Up @@ -82,7 +82,7 @@ func getDownloadFile(c *gin.Context) {
}

s, ok := manager.Get(token.ServerUuid)
if !ok || !token.IsUniqueRequest() {
if !ok || !token.IsUniqueRequest() || !token.HasScope(tokens.FileDownload) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on this server.",
})
Expand Down
2 changes: 1 addition & 1 deletion router/router_server_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ func postServerUploadFiles(c *gin.Context) {
}

s, ok := manager.Get(token.ServerUuid)
if !ok || !token.IsUniqueRequest() {
if !ok || !token.IsUniqueRequest() || !token.HasScope(tokens.FileUpload) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on this server.",
})
Expand Down
7 changes: 6 additions & 1 deletion router/router_transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func postTransfers(c *gin.Context) {
if len(auth) != 2 || auth[0] != "Bearer" {
c.Header("WWW-Authenticate", "Bearer")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "The required authorization heads were not present in the request.",
"error": "The required authorization headers were not present in the request.",
})
return
}
Expand All @@ -42,6 +42,11 @@ func postTransfers(c *gin.Context) {
return
}

if !token.HasScope(tokens.ServerTransfer) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden."})
return
}

manager := middleware.ExtractManager(c)
u, err := uuid.Parse(token.Subject)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions router/tokens/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

type BackupPayload struct {
jwt.Payload
Scoped

ServerUuid string `json:"server_uuid"`
BackupUuid string `json:"backup_uuid"`
Expand Down
2 changes: 2 additions & 0 deletions router/tokens/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (

type FilePayload struct {
jwt.Payload
Scoped

FilePath string `json:"file_path"`
ServerUuid string `json:"server_uuid"`
UniqueId string `json:"unique_id"`
Expand Down
33 changes: 33 additions & 0 deletions router/tokens/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package tokens

import (
"strings"
)

type JwtScope string

const (
Websocket = JwtScope("websocket")
FileUpload = JwtScope("file-upload")
FileDownload = JwtScope("file-download")
BackupDownload = JwtScope("backup-download")
ServerTransfer = JwtScope("transfer")
)

type Scoped struct {
Scope string `json:"scope"`
}

func (s Scoped) Scopes() []string {
return strings.Split(s.Scope, " ")
}

func (s Scoped) HasScope(scope JwtScope) bool {
for _, v := range s.Scopes() {
if v == string(scope) {
return true
}
}

return false
}
1 change: 1 addition & 0 deletions router/tokens/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

type TransferPayload struct {
jwt.Payload
Scoped
}

// GetPayload returns the JWT payload.
Expand Down
1 change: 1 addition & 0 deletions router/tokens/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

type UploadPayload struct {
jwt.Payload
Scoped

ServerUuid string `json:"server_uuid"`
UserUuid string `json:"user_uuid"`
Expand Down
1 change: 1 addition & 0 deletions router/tokens/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func DenyForServer(s string, u string) {
type WebsocketPayload struct {
jwt.Payload
sync.RWMutex
Scoped

UserUUID string `json:"user_uuid"`
ServerUUID string `json:"server_uuid"`
Expand Down
4 changes: 2 additions & 2 deletions router/websocket/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func NewTokenPayload(token []byte) (*tokens.WebsocketPayload, error) {
return nil, ErrJwtOnDenylist
}

if !payload.HasPermission(PermissionConnect) {
if !payload.HasPermission(PermissionConnect) || !payload.HasScope(tokens.Websocket) {
return nil, ErrJwtNoConnectPerm
}

Expand Down Expand Up @@ -213,7 +213,7 @@ func (h *Handler) TokenValid() error {
return ErrJwtOnDenylist
}

if !j.HasPermission(PermissionConnect) {
if !j.HasPermission(PermissionConnect) || !j.HasScope(tokens.Websocket) {
return ErrJwtNoConnectPerm
}

Expand Down