Skip to content

Commit 55045f7

Browse files
Pranav GoyalGitHub Enterprise
authored andcommitted
Merge pull request #574 from mq-cloudpak/pranav-2766-asoc-pathtraversal-93015-r1
Code changes to provide sanitised file paths and resolve Asoc Path Traversal vulnerability in v9.3.0.15-r1 (LTS)
2 parents 7fabcac + 9931a2c commit 55045f7

File tree

7 files changed

+128
-37
lines changed

7 files changed

+128
-37
lines changed

cmd/runmqserver/qmgr.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package main
1818
import (
1919
"fmt"
2020
"os"
21-
"path/filepath"
2221
"regexp"
2322
"strconv"
2423
"strings"
@@ -27,6 +26,7 @@ import (
2726
containerruntime "github.com/ibm-messaging/mq-container/internal/containerruntime"
2827
"github.com/ibm-messaging/mq-container/internal/mqscredact"
2928
"github.com/ibm-messaging/mq-container/internal/mqversion"
29+
"github.com/ibm-messaging/mq-container/internal/pathutils"
3030
"github.com/ibm-messaging/mq-container/internal/ready"
3131
)
3232

@@ -83,7 +83,7 @@ func createQueueManager(name string, devMode bool) (bool, error) {
8383

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

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

internal/filecheck/filecheck.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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

@@ -175,15 +176,15 @@ func generateAllKeystores(keystoreDir string, p12TruststoreRequired bool) (TLSSt
175176
}
176177

177178
// Create the CMS Keystore
178-
cmsKeystore.Keystore = keystore.NewCMSKeyStore(filepath.Join(keystoreDir, cmsKeystoreName), cmsKeystore.Password)
179+
cmsKeystore.Keystore = keystore.NewCMSKeyStore(pathutils.CleanPath(keystoreDir, cmsKeystoreName), cmsKeystore.Password)
179180
err = cmsKeystore.Keystore.Create()
180181
if err != nil {
181182
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create CMS Keystore: %v", err)
182183
}
183184

184185
// Create the PKCS#12 Truststore (if required)
185186
if p12TruststoreRequired {
186-
p12Truststore.Keystore = keystore.NewPKCS12KeyStore(filepath.Join(keystoreDir, p12TruststoreName), p12Truststore.Password)
187+
p12Truststore.Keystore = keystore.NewPKCS12KeyStore(pathutils.CleanPath(keystoreDir, p12TruststoreName), p12Truststore.Password)
187188
err = p12Truststore.Keystore.Create()
188189
if err != nil {
189190
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create PKCS#12 Truststore: %v", err)
@@ -205,7 +206,7 @@ func processKeys(tlsStore *TLSStore, keystoreDir string, keyDir string) (string,
205206

206207
// Process each set of keys - each set should contain files: *.key & *.crt
207208
for _, keySet := range keyList {
208-
keys, _ := os.ReadDir(filepath.Join(keyDir, keySet.Name()))
209+
keys, _ := os.ReadDir(pathutils.CleanPath(keyDir, keySet.Name()))
209210

210211
// Ensure the label of the set of keys does not match the name of the PKCS#12 Truststore
211212
if keySet.Name() == p12TruststoreName[0:len(p12TruststoreName)-len(filepath.Ext(p12TruststoreName))] {
@@ -234,16 +235,17 @@ func processKeys(tlsStore *TLSStore, keystoreDir string, keyDir string) (string,
234235
if err != nil {
235236
return "", fmt.Errorf("Failed to encode PKCS#12 Keystore %s: %v", keySet.Name()+".p12", err)
236237
}
238+
keystorePath := pathutils.CleanPath(keystoreDir, keySet.Name()+".p12")
237239
// #nosec G306 - this gives permissions to owner/s group only.
238-
err = os.WriteFile(filepath.Join(keystoreDir, keySet.Name()+".p12"), file, 0644)
240+
err = os.WriteFile(keystorePath, file, 0644)
239241
if err != nil {
240-
return "", fmt.Errorf("Failed to write PKCS#12 Keystore %s: %v", filepath.Join(keystoreDir, keySet.Name()+".p12"), err)
242+
return "", fmt.Errorf("Failed to write PKCS#12 Keystore %s: %v", keystorePath, err)
241243
}
242244

243245
// Import the new PKCS#12 Keystore into the CMS Keystore
244-
err = tlsStore.Keystore.Keystore.Import(filepath.Join(keystoreDir, keySet.Name()+".p12"), tlsStore.Keystore.Password)
246+
err = tlsStore.Keystore.Keystore.Import(keystorePath, tlsStore.Keystore.Password)
245247
if err != nil {
246-
return "", fmt.Errorf("Failed to import keys from %s into CMS Keystore: %v", filepath.Join(keystoreDir, keySet.Name()+".p12"), err)
248+
return "", fmt.Errorf("Failed to import keys from %s into CMS Keystore: %v", keystorePath, err)
247249
}
248250

249251
// Relabel the certificate in the CMS Keystore
@@ -271,14 +273,15 @@ func processTrustCertificates(tlsStore *TLSStore, trustDir string) error {
271273

272274
// Process each set of keys
273275
for _, trustSet := range trustList {
274-
keys, _ := os.ReadDir(filepath.Join(trustDir, trustSet.Name()))
276+
keys, _ := os.ReadDir(pathutils.CleanPath(trustDir, trustSet.Name()))
275277

276278
for _, key := range keys {
277279
if strings.HasSuffix(key.Name(), ".crt") {
280+
trustSetPath := pathutils.CleanPath(trustDir, trustSet.Name(), key.Name())
278281
// #nosec G304 - filename variable is derived from contents of 'trustDir' which is a defined constant
279-
file, err := os.ReadFile(filepath.Join(trustDir, trustSet.Name(), key.Name()))
282+
file, err := os.ReadFile(trustSetPath)
280283
if err != nil {
281-
return fmt.Errorf("Failed to read file %s: %v", filepath.Join(trustDir, trustSet.Name(), key.Name()), err)
284+
return fmt.Errorf("Failed to read file %s: %v", trustSetPath, err)
282285
}
283286

284287
for string(file) != "" {
@@ -334,15 +337,16 @@ func processPrivateKey(keyDir string, keySetName string, keys []os.DirEntry) (in
334337

335338
for _, key := range keys {
336339

340+
privateKeyPath := pathutils.CleanPath(keyDir, keySetName, key.Name())
337341
if strings.HasSuffix(key.Name(), ".key") {
338342
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
339-
file, err := os.ReadFile(filepath.Join(keyDir, keySetName, key.Name()))
343+
file, err := os.ReadFile(privateKeyPath)
340344
if err != nil {
341-
return nil, "", fmt.Errorf("Failed to read private key %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
345+
return nil, "", fmt.Errorf("Failed to read private key %s: %v", privateKeyPath, err)
342346
}
343347
block, _ := pem.Decode(file)
344348
if block == nil {
345-
return nil, "", fmt.Errorf("Failed to decode private key %s: pem.Decode returned nil", filepath.Join(keyDir, keySetName, key.Name()))
349+
return nil, "", fmt.Errorf("Failed to decode private key %s: pem.Decode returned nil", privateKeyPath)
346350
}
347351

348352
// Check if the private key is PKCS1
@@ -351,7 +355,7 @@ func processPrivateKey(keyDir string, keySetName string, keys []os.DirEntry) (in
351355
// Check if the private key is PKCS8
352356
privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
353357
if err != nil {
354-
return nil, "", fmt.Errorf("Failed to parse private key %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
358+
return nil, "", fmt.Errorf("Failed to parse private key %s: %v", privateKeyPath, err)
355359
}
356360
}
357361
keyPrefix = key.Name()[0 : len(key.Name())-len(filepath.Ext(key.Name()))]
@@ -369,19 +373,20 @@ func processCertificates(keyDir string, keySetName, keyPrefix string, keys []os.
369373

370374
for _, key := range keys {
371375

376+
keystorePath := pathutils.CleanPath(keyDir, keySetName, key.Name())
372377
if strings.HasPrefix(key.Name(), keyPrefix) && strings.HasSuffix(key.Name(), ".crt") {
373378
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
374-
file, err := os.ReadFile(filepath.Join(keyDir, keySetName, key.Name()))
379+
file, err := os.ReadFile(keystorePath)
375380
if err != nil {
376-
return nil, nil, fmt.Errorf("Failed to read public certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
381+
return nil, nil, fmt.Errorf("Failed to read public certificate %s: %v", keystorePath, err)
377382
}
378383
block, _ := pem.Decode(file)
379384
if block == nil {
380-
return nil, nil, fmt.Errorf("Failed to decode public certificate %s: pem.Decode returned nil", filepath.Join(keyDir, keySetName, key.Name()))
385+
return nil, nil, fmt.Errorf("Failed to decode public certificate %s: pem.Decode returned nil", keystorePath)
381386
}
382387
publicCertificate, err = x509.ParseCertificate(block.Bytes)
383388
if err != nil {
384-
return nil, nil, fmt.Errorf("Failed to parse public certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
389+
return nil, nil, fmt.Errorf("Failed to parse public certificate %s: %v", keystorePath, err)
385390
}
386391

387392
// Add to known certificates for the CMS Keystore
@@ -392,9 +397,9 @@ func processCertificates(keyDir string, keySetName, keyPrefix string, keys []os.
392397

393398
} else if strings.HasSuffix(key.Name(), ".crt") {
394399
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
395-
file, err := os.ReadFile(filepath.Join(keyDir, keySetName, key.Name()))
400+
file, err := os.ReadFile(keystorePath)
396401
if err != nil {
397-
return nil, nil, fmt.Errorf("Failed to read CA certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
402+
return nil, nil, fmt.Errorf("Failed to read CA certificate %s: %v", keystorePath, err)
398403
}
399404

400405
for string(file) != "" {
@@ -420,7 +425,7 @@ func processCertificates(keyDir string, keySetName, keyPrefix string, keys []os.
420425

421426
certificate, err := x509.ParseCertificate(block.Bytes)
422427
if err != nil {
423-
return nil, nil, fmt.Errorf("Failed to parse CA certificate %s: %v", filepath.Join(keyDir, keySetName, key.Name()), err)
428+
return nil, nil, fmt.Errorf("Failed to parse CA certificate %s: %v", keystorePath, err)
424429
}
425430
caCertificate = append(caCertificate, certificate)
426431
}
@@ -467,7 +472,7 @@ func relabelCertificate(newLabel string, cmsKeystore *KeyStoreData) error {
467472
// addCertificatesToTruststore adds trust certificates to the PKCS#12 Truststore
468473
func addCertificatesToTruststore(p12Truststore *KeyStoreData) error {
469474

470-
temporaryPemFile := filepath.Join("/tmp", "trust.pem")
475+
temporaryPemFile := pathutils.CleanPath("/tmp", "trust.pem")
471476
_, err := os.Stat(temporaryPemFile)
472477
if err == nil {
473478
err = os.Remove(temporaryPemFile)
@@ -509,7 +514,7 @@ func addCertificatesToTruststore(p12Truststore *KeyStoreData) error {
509514
// addCertificatesToCMSKeystore adds trust certificates to the CMS keystore
510515
func addCertificatesToCMSKeystore(cmsKeystore *KeyStoreData) error {
511516

512-
temporaryPemFile := filepath.Join("/tmp", "cmsTrust.pem")
517+
temporaryPemFile := pathutils.CleanPath("/tmp", "cmsTrust.pem")
513518
_, err := os.Stat(temporaryPemFile)
514519
if err == nil {
515520
err = os.Remove(temporaryPemFile)

internal/tls/tls_web.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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")

pkg/mqini/mqini.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ import (
2121
"bufio"
2222
"errors"
2323
"os"
24-
"path/filepath"
2524
"strings"
2625

2726
"github.com/ibm-messaging/mq-container/internal/command"
27+
"github.com/ibm-messaging/mq-container/internal/pathutils"
2828
)
2929

3030
// QueueManager describe high-level configuration information for a queue manager
@@ -78,7 +78,7 @@ func GetQueueManager(name string) (*QueueManager, error) {
7878
// GetErrorLogDirectory returns the directory holding the error logs for the
7979
// specified queue manager
8080
func GetErrorLogDirectory(qm *QueueManager) string {
81-
return filepath.Join(GetDataDirectory(qm), "errors")
81+
return pathutils.CleanPath(GetDataDirectory(qm), "errors")
8282
}
8383

8484
// GetDataDirectory returns the data directory for the specified queue manager
@@ -87,6 +87,6 @@ func GetDataDirectory(qm *QueueManager) string {
8787
// Data path has been set explicitly (e.g. for multi-instance queue manager)
8888
return qm.DataPath
8989
} else {
90-
return filepath.Join(qm.Prefix, "qmgrs", qm.Directory)
90+
return pathutils.CleanPath(qm.Prefix, "qmgrs", qm.Directory)
9191
}
9292
}

0 commit comments

Comments
 (0)