Skip to content

Commit 565b0e3

Browse files
authored
Fix JWT Scope Security issue (#182)
Resolves impact from CVE-2026-52855
1 parent a0306eb commit 565b0e3

11 files changed

Lines changed: 67 additions & 19 deletions

File tree

router/router.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package router
22

33
import (
4+
"regexp"
5+
46
"emperror.dev/errors"
57
"github.com/apex/log"
68
"github.com/gin-gonic/gin"
@@ -10,6 +12,8 @@ import (
1012
wserver "github.com/pelican-dev/wings/server"
1113
)
1214

15+
var tokenRegex = regexp.MustCompile(`([?|&]token=)([^&]+)($|&)`)
16+
1317
// Configure configures the routing infrastructure for this daemon instance.
1418
func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
1519
gin.SetMode("release")
@@ -33,7 +37,9 @@ func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
3337
"status": params.StatusCode,
3438
"latency": params.Latency,
3539
"request_id": params.Keys["request_id"],
36-
}).Debugf("%s %s", params.MethodColor()+params.Method+params.ResetColor(), params.Path)
40+
}).Debugf("%s %s",
41+
params.Method,
42+
tokenRegex.ReplaceAllString(params.Path, "$1***$3"))
3743

3844
return ""
3945
}))

router/router_download.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func getDownloadBackup(c *gin.Context) {
2828
}
2929

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

8484
s, ok := manager.Get(token.ServerUuid)
85-
if !ok || !token.IsUniqueRequest() {
85+
if !ok || !token.IsUniqueRequest() || !token.HasScope(tokens.FileDownload) {
8686
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
8787
"error": "The requested resource was not found on this server.",
8888
})

router/router_server_files.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ func postServerDeleteFiles(c *gin.Context) {
240240
return s.Filesystem().SafeDeleteRecursively(pi)
241241
}
242242
})
243-
243+
244244
}
245245

246246
if err := g.Wait(); err != nil {
@@ -599,7 +599,7 @@ func postServerUploadFiles(c *gin.Context) {
599599
}
600600

601601
s, ok := manager.Get(token.ServerUuid)
602-
if !ok || !token.IsUniqueRequest() {
602+
if !ok || !token.IsUniqueRequest() || !token.HasScope(tokens.FileUpload) {
603603
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
604604
"error": "The requested resource was not found on this server.",
605605
})

router/router_transfer.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func postTransfers(c *gin.Context) {
3232
if len(auth) != 2 || auth[0] != "Bearer" {
3333
c.Header("WWW-Authenticate", "Bearer")
3434
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
35-
"error": "The required authorization heads were not present in the request.",
35+
"error": "The required authorization headers were not present in the request.",
3636
})
3737
return
3838
}
@@ -43,6 +43,11 @@ func postTransfers(c *gin.Context) {
4343
return
4444
}
4545

46+
if !token.HasScope(tokens.ServerTransfer) {
47+
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden."})
48+
return
49+
}
50+
4651
manager := middleware.ExtractManager(c)
4752
u, err := uuid.Parse(token.Subject)
4853
if err != nil {
@@ -142,14 +147,13 @@ func postTransfers(c *gin.Context) {
142147
return
143148
}
144149

145-
146150
// Used to read the file and checksum from the request body.
147151
mr := multipart.NewReader(c.Request.Body, params["boundary"])
148152

149153
var (
150-
hasArchive bool
151-
archiveChecksum string
152-
archiveChecksumReceived string
154+
hasArchive bool
155+
archiveChecksum string
156+
archiveChecksumReceived string
153157
backupChecksumsCalculated = make(map[string]string)
154158
backupChecksumsReceived = make(map[string]string)
155159
)
@@ -208,7 +212,7 @@ out:
208212

209213
case name == "install_logs":
210214
trnsfr.Log().Debug("received install logs")
211-
215+
212216
// Create install log directory if it doesn't exist
213217
cfg := config.Get()
214218
installLogDir := filepath.Join(cfg.System.LogDirectory, "install")
@@ -217,33 +221,33 @@ out:
217221
trnsfr.Log().WithError(err).Warn("failed to create install log directory, skipping")
218222
break
219223
}
220-
224+
221225
// Use the correct install log path with server UUID
222226
installLogPath := filepath.Join(installLogDir, trnsfr.Server.ID()+".log")
223-
227+
224228
// Create the install log file
225229
installLogFile, err := os.Create(installLogPath)
226230
if err != nil {
227231
// Don't fail transfer for install logs, just log and continue
228232
trnsfr.Log().WithError(err).Warn("failed to create install log file, skipping")
229233
break
230234
}
231-
235+
232236
// Stream the install logs to file
233237
if _, err := io.Copy(installLogFile, p); err != nil {
234238
installLogFile.Close()
235239
// Don't fail transfer for install logs, just log and continue
236240
trnsfr.Log().WithError(err).Warn("failed to stream install logs to file, skipping")
237241
break
238242
}
239-
243+
240244
if err := installLogFile.Close(); err != nil {
241245
// Don't fail transfer for install logs, just log and continue
242246
trnsfr.Log().WithError(err).Warn("failed to close install log file")
243247
}
244-
248+
245249
trnsfr.Log().WithField("path", installLogPath).Debug("install logs saved successfully")
246-
250+
247251
case strings.HasPrefix(name, "backup_"):
248252
backupName := strings.TrimPrefix(name, "backup_")
249253
trnsfr.Log().WithField("backup", backupName).Debug("received backup file")

router/tokens/backup.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type BackupPayload struct {
1010
ServerUuid string `json:"server_uuid"`
1111
BackupUuid string `json:"backup_uuid"`
1212
UniqueId string `json:"unique_id"`
13+
Scoped
1314
}
1415

1516
// Returns the JWT payload.

router/tokens/file.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type FilePayload struct {
99
FilePath string `json:"file_path"`
1010
ServerUuid string `json:"server_uuid"`
1111
UniqueId string `json:"unique_id"`
12+
Scoped
1213
}
1314

1415
// Returns the JWT payload.

router/tokens/token.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package tokens
2+
3+
import (
4+
"strings"
5+
)
6+
7+
type JwtScope string
8+
9+
const (
10+
Websocket = JwtScope("websocket")
11+
FileUpload = JwtScope("file-upload")
12+
FileDownload = JwtScope("file-download")
13+
BackupDownload = JwtScope("backup-download")
14+
ServerTransfer = JwtScope("transfer")
15+
)
16+
17+
type Scoped struct {
18+
Scope string `json:"scope"`
19+
}
20+
21+
func (s Scoped) Scopes() []string {
22+
return strings.Split(s.Scope, " ")
23+
}
24+
25+
func (s Scoped) HasScope(scope JwtScope) bool {
26+
for _, v := range s.Scopes() {
27+
if v == string(scope) {
28+
return true
29+
}
30+
}
31+
32+
return false
33+
}

router/tokens/transfer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
type TransferPayload struct {
88
jwt.Payload
9+
Scoped
910
}
1011

1112
// GetPayload returns the JWT payload.

router/tokens/upload.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type UploadPayload struct {
1010
ServerUuid string `json:"server_uuid"`
1111
UserUuid string `json:"user_uuid"`
1212
UniqueId string `json:"unique_id"`
13+
Scoped
1314
}
1415

1516
// Returns the JWT payload.

router/tokens/websocket.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func DenyForServer(s string, u string) {
5252
type WebsocketPayload struct {
5353
jwt.Payload
5454
sync.RWMutex
55+
Scoped
5556

5657
UserUUID string `json:"user_uuid"`
5758
ServerUUID string `json:"server_uuid"`

0 commit comments

Comments
 (0)