Skip to content

Commit 9b9c6f8

Browse files
Pranav GoyalGitHub Enterprise
authored andcommitted
Merge pull request #573 from mq-cloudpak/pranav-2766-asoc-pathtraversal-9341-r1
Code changes to provide sanitised file paths and resolve Asoc Path Traversal vulnerability in v9.3.4.1-r1 (CD)
2 parents 627e1a8 + 18651fe commit 9b9c6f8

File tree

7 files changed

+132
-41
lines changed

7 files changed

+132
-41
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, replaceCharsInQMName(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 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.
@@ -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

@@ -191,7 +192,7 @@ func generateAllKeystores(keystoreDir string, p12TruststoreRequired bool, native
191192
}
192193
// Create the CMS Keystore if we have been provided keys and certificates
193194
if haveKeysAndCerts(keysDirectory) || haveKeysAndCerts(trustDirDefault) {
194-
cmsKeystore.Keystore = keystore.NewCMSKeyStore(filepath.Join(keystoreDir, cmsKeystoreName), cmsKeystore.Password)
195+
cmsKeystore.Keystore = keystore.NewCMSKeyStore(pathutils.CleanPath(keystoreDir, cmsKeystoreName), cmsKeystore.Password)
195196
err = cmsKeystore.Keystore.Create()
196197
if err != nil {
197198
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create CMS Keystore: %v", err)
@@ -200,7 +201,7 @@ func generateAllKeystores(keystoreDir string, p12TruststoreRequired bool, native
200201

201202
// Create the PKCS#12 Truststore (if required)
202203
if p12TruststoreRequired {
203-
p12Truststore.Keystore = keystore.NewPKCS12KeyStore(filepath.Join(keystoreDir, p12TruststoreName), p12Truststore.Password)
204+
p12Truststore.Keystore = keystore.NewPKCS12KeyStore(pathutils.CleanPath(keystoreDir, p12TruststoreName), p12Truststore.Password)
204205
err = p12Truststore.Keystore.Create()
205206
if err != nil {
206207
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create PKCS#12 Truststore: %v", err)
@@ -221,7 +222,7 @@ func processKeys(tlsStore *TLSStore, keystoreDir string, keyDir string) (string,
221222
if err == nil && len(keyList) > 0 {
222223
// Process each set of keys - each set should contain files: *.key & *.crt
223224
for _, keySet := range keyList {
224-
keys, _ := os.ReadDir(filepath.Join(keyDir, keySet.Name()))
225+
keys, _ := os.ReadDir(pathutils.CleanPath(keyDir, keySet.Name()))
225226

226227
// Ensure the label of the set of keys does not match the name of the PKCS#12 Truststore
227228
if keySet.Name() == p12TruststoreName[0:len(p12TruststoreName)-len(filepath.Ext(p12TruststoreName))] {
@@ -263,16 +264,17 @@ func processKeys(tlsStore *TLSStore, keystoreDir string, keyDir string) (string,
263264
if err != nil {
264265
return "", fmt.Errorf("Failed to encode PKCS#12 Keystore %s: %v", keySet.Name()+".p12", err)
265266
}
267+
keystorePath := pathutils.CleanPath(keystoreDir, keySet.Name()+".p12")
266268
// #nosec G306 - this gives permissions to owner/s group only.
267-
err = os.WriteFile(filepath.Join(keystoreDir, keySet.Name()+".p12"), file, 0644)
269+
err = os.WriteFile(keystorePath, file, 0644)
268270
if err != nil {
269-
return "", fmt.Errorf("Failed to write PKCS#12 Keystore %s: %v", filepath.Join(keystoreDir, keySet.Name()+".p12"), err)
271+
return "", fmt.Errorf("Failed to write PKCS#12 Keystore %s: %v", keystorePath, err)
270272
}
271273

272274
// Import the new PKCS#12 Keystore into the CMS Keystore
273-
err = tlsStore.Keystore.Keystore.Import(filepath.Join(keystoreDir, keySet.Name()+".p12"), tlsStore.Keystore.Password)
275+
err = tlsStore.Keystore.Keystore.Import(keystorePath, tlsStore.Keystore.Password)
274276
if err != nil {
275-
return "", fmt.Errorf("Failed to import keys from %s into CMS Keystore: %v", filepath.Join(keystoreDir, keySet.Name()+".p12"), err)
277+
return "", fmt.Errorf("Failed to import keys from %s into CMS Keystore: %v", keystorePath, err)
276278
}
277279

278280
// Relabel the certificate in the CMS Keystore
@@ -300,14 +302,15 @@ func processTrustCertificates(tlsStore *TLSStore, trustDir string) error {
300302

301303
// Process each set of keys
302304
for _, trustSet := range trustList {
303-
keys, _ := os.ReadDir(filepath.Join(trustDir, trustSet.Name()))
305+
keys, _ := os.ReadDir(pathutils.CleanPath(trustDir, trustSet.Name()))
304306

305307
for _, key := range keys {
306308
if strings.HasSuffix(key.Name(), ".crt") {
309+
trustSetPath := pathutils.CleanPath(trustDir, trustSet.Name(), key.Name())
307310
// #nosec G304 - filename variable is derived from contents of 'trustDir' which is a defined constant
308-
file, err := os.ReadFile(filepath.Join(trustDir, trustSet.Name(), key.Name()))
311+
file, err := os.ReadFile(trustSetPath)
309312
if err != nil {
310-
return fmt.Errorf("Failed to read file %s: %v", filepath.Join(trustDir, trustSet.Name(), key.Name()), err)
313+
return fmt.Errorf("Failed to read file %s: %v", trustSetPath, err)
311314
}
312315

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

364367
for _, key := range keys {
365368

369+
privateKeyPath := pathutils.CleanPath(keyDir, keySetName, key.Name())
366370
if strings.HasSuffix(key.Name(), ".key") {
367371
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
368-
file, err := os.ReadFile(filepath.Join(keyDir, keySetName, key.Name()))
372+
file, err := os.ReadFile(privateKeyPath)
369373
if err != nil {
370-
return nil, "", fmt.Errorf("Failed to read private key %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
374+
return nil, "", fmt.Errorf("Failed to read private key %s: %v", privateKeyPath, err)
371375
}
372376
block, _ := pem.Decode(file)
373377
if block == nil {
374-
return nil, "", fmt.Errorf("Failed to decode private key %s: pem.Decode returned nil", filepath.Join(keyDir, keySetName, key.Name()))
378+
return nil, "", fmt.Errorf("Failed to decode private key %s: pem.Decode returned nil", privateKeyPath)
375379
}
376380

377381
// Check if the private key is PKCS1
@@ -380,7 +384,7 @@ func processPrivateKey(keyDir string, keySetName string, keys []os.DirEntry) (in
380384
// Check if the private key is PKCS8
381385
privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
382386
if err != nil {
383-
return nil, "", fmt.Errorf("Failed to parse private key %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
387+
return nil, "", fmt.Errorf("Failed to parse private key %s: %v", privateKeyPath, err)
384388
}
385389
}
386390
keyPrefix = key.Name()[0 : len(key.Name())-len(filepath.Ext(key.Name()))]
@@ -398,19 +402,20 @@ func processCertificates(keyDir string, keySetName, keyPrefix string, keys []os.
398402

399403
for _, key := range keys {
400404

405+
keystorePath := pathutils.CleanPath(keyDir, keySetName, key.Name())
401406
if strings.HasPrefix(key.Name(), keyPrefix) && strings.HasSuffix(key.Name(), ".crt") {
402407
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
403-
file, err := os.ReadFile(filepath.Join(keyDir, keySetName, key.Name()))
408+
file, err := os.ReadFile(keystorePath)
404409
if err != nil {
405-
return nil, nil, fmt.Errorf("Failed to read public certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
410+
return nil, nil, fmt.Errorf("Failed to read public certificate %s: %v", keystorePath, err)
406411
}
407412
block, _ := pem.Decode(file)
408413
if block == nil {
409-
return nil, nil, fmt.Errorf("Failed to decode public certificate %s: pem.Decode returned nil", filepath.Join(keyDir, keySetName, key.Name()))
414+
return nil, nil, fmt.Errorf("Failed to decode public certificate %s: pem.Decode returned nil", keystorePath)
410415
}
411416
publicCertificate, err = x509.ParseCertificate(block.Bytes)
412417
if err != nil {
413-
return nil, nil, fmt.Errorf("Failed to parse public certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
418+
return nil, nil, fmt.Errorf("Failed to parse public certificate %s: %v", keystorePath, err)
414419
}
415420

416421
// Add to known certificates for the CMS Keystore
@@ -421,9 +426,9 @@ func processCertificates(keyDir string, keySetName, keyPrefix string, keys []os.
421426

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

429434
for string(file) != "" {
@@ -449,7 +454,7 @@ func processCertificates(keyDir string, keySetName, keyPrefix string, keys []os.
449454

450455
certificate, err := x509.ParseCertificate(block.Bytes)
451456
if err != nil {
452-
return nil, nil, fmt.Errorf("Failed to parse CA certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
457+
return nil, nil, fmt.Errorf("Failed to parse CA certificate %s: %v", keystorePath, err)
453458
}
454459
caCertificate = append(caCertificate, certificate)
455460
}
@@ -496,7 +501,7 @@ func relabelCertificate(newLabel string, cmsKeystore *KeyStoreData) error {
496501
// addCertificatesToTruststore adds trust certificates to the PKCS#12 Truststore
497502
func addCertificatesToTruststore(p12Truststore *KeyStoreData) error {
498503

499-
temporaryPemFile := filepath.Join("/tmp", "trust.pem")
504+
temporaryPemFile := pathutils.CleanPath("/tmp", "trust.pem")
500505
_, err := os.Stat(temporaryPemFile)
501506
if err == nil {
502507
err = os.Remove(temporaryPemFile)
@@ -538,7 +543,7 @@ func addCertificatesToTruststore(p12Truststore *KeyStoreData) error {
538543
// addCertificatesToCMSKeystore adds trust certificates to the CMS keystore
539544
func addCertificatesToCMSKeystore(cmsKeystore *KeyStoreData) error {
540545

541-
temporaryPemFile := filepath.Join("/tmp", "cmsTrust.pem")
546+
temporaryPemFile := pathutils.CleanPath("/tmp", "cmsTrust.pem")
542547
_, err := os.Stat(temporaryPemFile)
543548
if err == nil {
544549
err = os.Remove(temporaryPemFile)
@@ -644,7 +649,7 @@ func haveKeysAndCerts(keyDir string) bool {
644649
for _, fileInfo := range fileList {
645650
// Keys and certs will be supplied in an user defined subdirectory.
646651
// Do a listing of the subdirectory and then search for .key and .cert files
647-
keys, _ := os.ReadDir(filepath.Join(keyDir, fileInfo.Name()))
652+
keys, _ := os.ReadDir(pathutils.CleanPath(keyDir, fileInfo.Name()))
648653
for _, key := range keys {
649654
if strings.HasSuffix(key.Name(), ".key") || strings.HasSuffix(key.Name(), ".crt") {
650655
// We found at least one key/crt file.

internal/tls/tls_web.go

Lines changed: 3 additions & 3 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.
@@ -18,10 +18,10 @@ package tls
1818
import (
1919
"fmt"
2020
"os"
21-
"path/filepath"
2221

2322
"github.com/ibm-messaging/mq-container/internal/keystore"
2423
"github.com/ibm-messaging/mq-container/internal/mqtemplate"
24+
"github.com/ibm-messaging/mq-container/internal/pathutils"
2525
"github.com/ibm-messaging/mq-container/pkg/logger"
2626
)
2727

@@ -54,7 +54,7 @@ func ConfigureWebKeystore(p12Truststore KeyStoreData, keyLabel string) (string,
5454
if keyLabel != "" {
5555
webKeystore = keyLabel + ".p12"
5656
}
57-
webKeystoreFile := filepath.Join(keystoreDirDefault, webKeystore)
57+
webKeystoreFile := pathutils.CleanPath(keystoreDirDefault, webKeystore)
5858

5959
// Check if a new self-signed certificate should be generated
6060
if keyLabel == "" {

0 commit comments

Comments
 (0)