Skip to content

Commit 406160c

Browse files
Pranav GoyalGitHub Enterprise
authored andcommitted
Merge pull request #572 from mq-cloudpak/pranav-2766-asoc-pathtraversal-9333-r1
Code changes to provide sanitised file paths and resolve Asoc Path Traversal vulnerability in v9.3.3-r1
2 parents 94d3969 + fa991d0 commit 406160c

File tree

7 files changed

+134
-43
lines changed

7 files changed

+134
-43
lines changed

cmd/runmqserver/qmgr.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
© Copyright IBM Corporation 2017, 2023
2+
© Copyright IBM Corporation 2017, 2024
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -19,7 +19,6 @@ import (
1919
"context"
2020
"fmt"
2121
"os"
22-
"path/filepath"
2322
"regexp"
2423
"strconv"
2524
"strings"
@@ -28,6 +27,7 @@ import (
2827
containerruntime "github.com/ibm-messaging/mq-container/internal/containerruntime"
2928
"github.com/ibm-messaging/mq-container/internal/mqscredact"
3029
"github.com/ibm-messaging/mq-container/internal/mqversion"
30+
"github.com/ibm-messaging/mq-container/internal/pathutils"
3131
"github.com/ibm-messaging/mq-container/internal/ready"
3232
)
3333

@@ -84,7 +84,7 @@ func createQueueManager(name string, devMode bool) (bool, error) {
8484

8585
// Check if 'qm.ini' configuration file exists for the queue manager
8686
// TODO : handle possible race condition - use a file lock?
87-
_, err = os.Stat(filepath.Join(dataDir, "qm.ini"))
87+
_, err = os.Stat(pathutils.CleanPath(dataDir, "qm.ini"))
8888
if err != nil {
8989
// If 'qm.ini' is not found - run 'crtmqm' to create a new queue manager
9090
args := getCreateQueueManagerArgs(mounts, name, devMode)
@@ -111,7 +111,7 @@ func createQueueManager(name string, devMode bool) (bool, error) {
111111
// readQMIni reads the qm.ini file and returns it as a byte array
112112
// This function is specific to comply with the nosec.
113113
func readQMIni(dataDir string) ([]byte, error) {
114-
qmgrDir := filepath.Join(dataDir, "qm.ini")
114+
qmgrDir := pathutils.CleanPath(dataDir, "qm.ini")
115115
// #nosec G304 - qmgrDir filepath is derived from dspmqinf
116116
iniFileBytes, err := os.ReadFile(qmgrDir)
117117
if err != nil {
@@ -233,9 +233,9 @@ func isStandbyQueueManager(name string) (bool, error) {
233233
}
234234

235235
func getQueueManagerDataDir(mounts map[string]string, name string) string {
236-
dataDir := filepath.Join("/var/mqm/qmgrs", name)
236+
dataDir := pathutils.CleanPath("/var/mqm/qmgrs", name)
237237
if _, ok := mounts["/mnt/mqm-data"]; ok {
238-
dataDir = filepath.Join("/mnt/mqm-data/qmgrs", name)
238+
dataDir = pathutils.CleanPath("/mnt/mqm-data/qmgrs", name)
239239
}
240240
return dataDir
241241
}
@@ -316,7 +316,7 @@ func updateQMini(qmname string) error {
316316
return err
317317
}
318318
dataDir := getQueueManagerDataDir(mounts, qmname)
319-
qmgrDir := filepath.Join(dataDir, "qm.ini")
319+
qmgrDir := pathutils.CleanPath(dataDir, "qm.ini")
320320
//read the initial version.
321321
// #nosec G304 - qmgrDir filepath is derived from dspmqinf
322322
iniFileBytes, err := os.ReadFile(qmgrDir)

internal/filecheck/filecheck.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
© Copyright IBM Corporation 2019
2+
© Copyright IBM Corporation 2024
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@ import (
2020
"fmt"
2121
"path/filepath"
2222
"strings"
23+
24+
"github.com/ibm-messaging/mq-container/internal/pathutils"
2325
)
2426

2527
// CheckFileSource checks the filename is valid
@@ -29,7 +31,7 @@ func CheckFileSource(fileName string) error {
2931

3032
prefixes := []string{"bin", "boot", "dev", "lib", "lib64", "proc", "sbin", "sys"}
3133
for _, prefix := range prefixes {
32-
if strings.HasPrefix(absFile, filepath.Join("/", prefix)) {
34+
if strings.HasPrefix(absFile, pathutils.CleanPath("/", prefix)) {
3335
return fmt.Errorf("Filename resolves to invalid path '%v'", absFile)
3436
}
3537
}

internal/pathutils/clean.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
© Copyright IBM Corporation 2024
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package pathutils contains code to provide sanitised file paths
18+
package pathutils
19+
20+
import (
21+
"path"
22+
"path/filepath"
23+
)
24+
25+
// CleanPath returns the result of joining a series of sanitised file paths (preventing directory traversal for each path)
26+
// If the first path is relative, a relative path is returned
27+
func CleanPath(paths ...string) string {
28+
if len(paths) == 0 {
29+
return ""
30+
}
31+
var combined string
32+
if !path.IsAbs(paths[0]) {
33+
combined = "./"
34+
}
35+
for _, part := range paths {
36+
combined = filepath.Join(combined, filepath.FromSlash(path.Clean("/"+part)))
37+
}
38+
return combined
39+
}

internal/pathutils/clean_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
© Copyright IBM Corporation 2024
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package pathutils
18+
19+
import (
20+
"strings"
21+
"testing"
22+
)
23+
24+
func TestClean(t *testing.T) {
25+
26+
tests := []struct {
27+
uncleaned string
28+
filepath string
29+
cleaned string
30+
}{
31+
{"/a/rooted/path", "some.file", "/a/rooted/path/some.file"},
32+
{"../../../a/relative/path", "abc.txt", "a/relative/path/abc.txt"},
33+
{"a/path" + ".p12", "some.file", "a/path.p12/some.file"},
34+
{"/", "bin", "/bin"},
35+
{"abc/def", "../../a/relative/path", "abc/def/a/relative/path"},
36+
}
37+
38+
for _, test := range tests {
39+
cleaned := CleanPath(test.uncleaned, test.filepath)
40+
41+
if !strings.EqualFold(cleaned, test.cleaned) {
42+
t.Fatalf("file path sanitisation failed. Expected %s but got %s\n", test.cleaned, cleaned)
43+
}
44+
}
45+
}

internal/tls/tls.go

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
© Copyright IBM Corporation 2019, 2023
2+
© Copyright IBM Corporation 2019, 2024
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@ import (
3333

3434
"github.com/ibm-messaging/mq-container/internal/keystore"
3535
"github.com/ibm-messaging/mq-container/internal/mqtemplate"
36+
"github.com/ibm-messaging/mq-container/internal/pathutils"
3637
"github.com/ibm-messaging/mq-container/pkg/logger"
3738
)
3839

@@ -200,7 +201,7 @@ func generateAllKeystores(keystoreDir string, p12TruststoreRequired bool, native
200201
}
201202
// Create the CMS Keystore if we have been provided keys and certificates
202203
if haveKeysAndCerts(keysDirectory) || haveKeysAndCerts(trustDirDefault) {
203-
cmsKeystore.Keystore = keystore.NewCMSKeyStore(filepath.Join(keystoreDir, cmsKeystoreName), cmsKeystore.Password)
204+
cmsKeystore.Keystore = keystore.NewCMSKeyStore(pathutils.CleanPath(keystoreDir, cmsKeystoreName), cmsKeystore.Password)
204205
err = cmsKeystore.Keystore.Create()
205206
if err != nil {
206207
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create CMS Keystore: %v", err)
@@ -209,7 +210,7 @@ func generateAllKeystores(keystoreDir string, p12TruststoreRequired bool, native
209210

210211
// Create the PKCS#12 Truststore (if required)
211212
if p12TruststoreRequired {
212-
p12Truststore.Keystore = keystore.NewPKCS12KeyStore(filepath.Join(keystoreDir, p12TruststoreName), p12Truststore.Password)
213+
p12Truststore.Keystore = keystore.NewPKCS12KeyStore(pathutils.CleanPath(keystoreDir, p12TruststoreName), p12Truststore.Password)
213214
err = p12Truststore.Keystore.Create()
214215
if err != nil {
215216
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create PKCS#12 Truststore: %v", err)
@@ -230,7 +231,7 @@ func processKeys(tlsStore *TLSStore, keystoreDir string, keyDir string) (string,
230231
if err == nil && len(keyList) > 0 {
231232
// Process each set of keys - each set should contain files: *.key & *.crt
232233
for _, keySet := range keyList {
233-
keys, _ := os.ReadDir(filepath.Join(keyDir, keySet.Name()))
234+
keys, _ := os.ReadDir(pathutils.CleanPath(keyDir, keySet.Name()))
234235

235236
// Ensure the label of the set of keys does not match the name of the PKCS#12 Truststore
236237
if keySet.Name() == p12TruststoreName[0:len(p12TruststoreName)-len(filepath.Ext(p12TruststoreName))] {
@@ -266,16 +267,17 @@ func processKeys(tlsStore *TLSStore, keystoreDir string, keyDir string) (string,
266267
if err != nil {
267268
return "", fmt.Errorf("Failed to encode PKCS#12 Keystore %s: %v", keySet.Name()+".p12", err)
268269
}
270+
keystorePath := pathutils.CleanPath(keystoreDir, keySet.Name()+".p12")
269271
// #nosec G306 - this gives permissions to owner/s group only.
270-
err = os.WriteFile(filepath.Join(keystoreDir, keySet.Name()+".p12"), file, 0644)
272+
err = os.WriteFile(keystorePath, file, 0644)
271273
if err != nil {
272-
return "", fmt.Errorf("Failed to write PKCS#12 Keystore %s: %v", filepath.Join(keystoreDir, keySet.Name()+".p12"), err)
274+
return "", fmt.Errorf("Failed to write PKCS#12 Keystore %s: %v", keystorePath, err)
273275
}
274276

275277
// Import the new PKCS#12 Keystore into the CMS Keystore
276-
err = tlsStore.Keystore.Keystore.Import(filepath.Join(keystoreDir, keySet.Name()+".p12"), tlsStore.Keystore.Password)
278+
err = tlsStore.Keystore.Keystore.Import(keystorePath, tlsStore.Keystore.Password)
277279
if err != nil {
278-
return "", fmt.Errorf("Failed to import keys from %s into CMS Keystore: %v", filepath.Join(keystoreDir, keySet.Name()+".p12"), err)
280+
return "", fmt.Errorf("Failed to import keys from %s into CMS Keystore: %v", keystorePath, err)
279281
}
280282

281283
// Relabel the certificate in the CMS Keystore
@@ -303,14 +305,15 @@ func processTrustCertificates(tlsStore *TLSStore, trustDir string) error {
303305

304306
// Process each set of keys
305307
for _, trustSet := range trustList {
306-
keys, _ := os.ReadDir(filepath.Join(trustDir, trustSet.Name()))
308+
keys, _ := os.ReadDir(pathutils.CleanPath(trustDir, trustSet.Name()))
307309

308310
for _, key := range keys {
309311
if strings.HasSuffix(key.Name(), ".crt") {
312+
trustSetPath := pathutils.CleanPath(trustDir, trustSet.Name(), key.Name())
310313
// #nosec G304 - filename variable is derived from contents of 'trustDir' which is a defined constant
311-
file, err := os.ReadFile(filepath.Join(trustDir, trustSet.Name(), key.Name()))
314+
file, err := os.ReadFile(trustSetPath)
312315
if err != nil {
313-
return fmt.Errorf("Failed to read file %s: %v", filepath.Join(trustDir, trustSet.Name(), key.Name()), err)
316+
return fmt.Errorf("Failed to read file %s: %v", trustSetPath, err)
314317
}
315318

316319
for string(file) != "" {
@@ -366,15 +369,16 @@ func processPrivateKey(keyDir string, keySetName string, keys []os.DirEntry) (in
366369

367370
for _, key := range keys {
368371

372+
privateKeyPath := pathutils.CleanPath(keyDir, keySetName, key.Name())
369373
if strings.HasSuffix(key.Name(), ".key") {
370374
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
371-
file, err := os.ReadFile(filepath.Join(keyDir, keySetName, key.Name()))
375+
file, err := os.ReadFile(privateKeyPath)
372376
if err != nil {
373-
return nil, "", fmt.Errorf("Failed to read private key %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
377+
return nil, "", fmt.Errorf("Failed to read private key %s: %v", privateKeyPath, err)
374378
}
375379
block, _ := pem.Decode(file)
376380
if block == nil {
377-
return nil, "", fmt.Errorf("Failed to decode private key %s: pem.Decode returned nil", filepath.Join(keyDir, keySetName, key.Name()))
381+
return nil, "", fmt.Errorf("Failed to decode private key %s: pem.Decode returned nil", privateKeyPath)
378382
}
379383

380384
// Check if the private key is PKCS1
@@ -383,7 +387,7 @@ func processPrivateKey(keyDir string, keySetName string, keys []os.DirEntry) (in
383387
// Check if the private key is PKCS8
384388
privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
385389
if err != nil {
386-
return nil, "", fmt.Errorf("Failed to parse private key %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
390+
return nil, "", fmt.Errorf("Failed to parse private key %s: %v", privateKeyPath, err)
387391
}
388392
}
389393
keyPrefix = key.Name()[0 : len(key.Name())-len(filepath.Ext(key.Name()))]
@@ -401,19 +405,20 @@ func processCertificates(keyDir string, keySetName, keyPrefix string, keys []os.
401405

402406
for _, key := range keys {
403407

408+
keystorePath := pathutils.CleanPath(keyDir, keySetName, key.Name())
404409
if strings.HasPrefix(key.Name(), keyPrefix) && strings.HasSuffix(key.Name(), ".crt") {
405410
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
406-
file, err := os.ReadFile(filepath.Join(keyDir, keySetName, key.Name()))
411+
file, err := os.ReadFile(keystorePath)
407412
if err != nil {
408-
return nil, nil, fmt.Errorf("Failed to read public certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
413+
return nil, nil, fmt.Errorf("Failed to read public certificate %s: %v", keystorePath, err)
409414
}
410415
block, _ := pem.Decode(file)
411416
if block == nil {
412-
return nil, nil, fmt.Errorf("Failed to decode public certificate %s: pem.Decode returned nil", filepath.Join(keyDir, keySetName, key.Name()))
417+
return nil, nil, fmt.Errorf("Failed to decode public certificate %s: pem.Decode returned nil", keystorePath)
413418
}
414419
publicCertificate, err = x509.ParseCertificate(block.Bytes)
415420
if err != nil {
416-
return nil, nil, fmt.Errorf("Failed to parse public certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
421+
return nil, nil, fmt.Errorf("Failed to parse public certificate %s: %v", keystorePath, err)
417422
}
418423

419424
// Add to known certificates for the CMS Keystore
@@ -424,9 +429,9 @@ func processCertificates(keyDir string, keySetName, keyPrefix string, keys []os.
424429

425430
} else if strings.HasSuffix(key.Name(), ".crt") {
426431
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
427-
file, err := os.ReadFile(filepath.Join(keyDir, keySetName, key.Name()))
432+
file, err := os.ReadFile(keystorePath)
428433
if err != nil {
429-
return nil, nil, fmt.Errorf("Failed to read CA certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
434+
return nil, nil, fmt.Errorf("Failed to read CA certificate %s: %v", keystorePath, err)
430435
}
431436

432437
for string(file) != "" {
@@ -452,7 +457,7 @@ func processCertificates(keyDir string, keySetName, keyPrefix string, keys []os.
452457

453458
certificate, err := x509.ParseCertificate(block.Bytes)
454459
if err != nil {
455-
return nil, nil, fmt.Errorf("Failed to parse CA certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
460+
return nil, nil, fmt.Errorf("Failed to parse CA certificate %s: %v", keystorePath, err)
456461
}
457462
caCertificate = append(caCertificate, certificate)
458463
}
@@ -499,7 +504,7 @@ func relabelCertificate(newLabel string, cmsKeystore *KeyStoreData) error {
499504
// addCertificatesToTruststore adds trust certificates to the PKCS#12 Truststore
500505
func addCertificatesToTruststore(p12Truststore *KeyStoreData) error {
501506

502-
temporaryPemFile := filepath.Join("/tmp", "trust.pem")
507+
temporaryPemFile := pathutils.CleanPath("/tmp", "trust.pem")
503508
_, err := os.Stat(temporaryPemFile)
504509
if err == nil {
505510
err = os.Remove(temporaryPemFile)
@@ -541,7 +546,7 @@ func addCertificatesToTruststore(p12Truststore *KeyStoreData) error {
541546
// addCertificatesToCMSKeystore adds trust certificates to the CMS keystore
542547
func addCertificatesToCMSKeystore(cmsKeystore *KeyStoreData) error {
543548

544-
temporaryPemFile := filepath.Join("/tmp", "cmsTrust.pem")
549+
temporaryPemFile := pathutils.CleanPath("/tmp", "cmsTrust.pem")
545550
_, err := os.Stat(temporaryPemFile)
546551
if err == nil {
547552
err = os.Remove(temporaryPemFile)
@@ -647,7 +652,7 @@ func haveKeysAndCerts(keyDir string) bool {
647652
for _, fileInfo := range fileList {
648653
// Keys and certs will be supplied in an user defined subdirectory.
649654
// Do a listing of the subdirectory and then search for .key and .cert files
650-
keys, _ := os.ReadDir(filepath.Join(keyDir, fileInfo.Name()))
655+
keys, _ := os.ReadDir(pathutils.CleanPath(keyDir, fileInfo.Name()))
651656
for _, key := range keys {
652657
if strings.HasSuffix(key.Name(), ".key") || strings.HasSuffix(key.Name(), ".crt") {
653658
// We found at least one key/crt file.

internal/tls/tls_web.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
© Copyright IBM Corporation 2019, 2021
2+
© Copyright IBM Corporation 2019, 2024
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -18,9 +18,9 @@ package tls
1818
import (
1919
"fmt"
2020
"os"
21-
"path/filepath"
2221

2322
"github.com/ibm-messaging/mq-container/internal/keystore"
23+
"github.com/ibm-messaging/mq-container/internal/pathutils"
2424
)
2525

2626
// webKeystoreDefault is the name of the default web server Keystore
@@ -37,8 +37,8 @@ func ConfigureWebTLS(keyLabel string) error {
3737
webConfigDir := "/etc/mqm/web/installations/Installation1/servers/mqweb"
3838
tls := "tls.xml"
3939

40-
tlsConfig := filepath.Join(webConfigDir, tls)
41-
newTLSConfig := filepath.Join(webConfigDir, tls+".tpl")
40+
tlsConfig := pathutils.CleanPath(webConfigDir, tls)
41+
newTLSConfig := pathutils.CleanPath(webConfigDir, tls+".tpl")
4242

4343
err := os.Remove(tlsConfig)
4444
if err != nil {
@@ -60,7 +60,7 @@ func ConfigureWebKeystore(p12Truststore KeyStoreData, webKeystore string) (strin
6060
if webKeystore == "" {
6161
webKeystore = webKeystoreDefault
6262
}
63-
webKeystoreFile := filepath.Join(keystoreDirDefault, webKeystore)
63+
webKeystoreFile := pathutils.CleanPath(keystoreDirDefault, webKeystore)
6464

6565
// Check if a new self-signed certificate should be generated
6666
genHostName := os.Getenv("MQ_GENERATE_CERTIFICATE_HOSTNAME")

0 commit comments

Comments
 (0)