Skip to content

Commit a7c9a6d

Browse files
authored
Merge pull request #165 from deploymenttheory/dev
Security fixes
2 parents 77fff1f + c5c552e commit a7c9a6d

File tree

6 files changed

+68
-21
lines changed

6 files changed

+68
-21
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Example configuration file (clientconfig.json):
9696
"Environment": {
9797
"InstanceName": "yourinstance",
9898
"OverrideBaseDomain": "",
99-
"APIType": "" // "jamfpro" / "graph"
99+
"APIType": "" // "jamfpro" / "msgraph"
100100
},
101101
"ClientOptions": {
102102
"LogLevel": "LogLevelDebug", // "LogLevelDebug" / "LogLevelInfo" / "LogLevelWarn" / "LogLevelError" / "LogLevelFatal" / "LogLevelPanic"

apiintegrations/jamfpro/jamfpro_api_request.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import (
77
"encoding/xml"
88
"io"
99
"mime/multipart"
10-
"os"
10+
"path/filepath"
1111
"strings"
1212

13+
"github.com/deploymenttheory/go-api-http-client/helpers"
1314
"github.com/deploymenttheory/go-api-http-client/logger"
1415
"go.uber.org/zap"
1516
)
@@ -55,7 +56,7 @@ func (j *JamfAPIHandler) MarshalRequest(body interface{}, method string, endpoin
5556
return data, nil
5657
}
5758

58-
// MarshalMultipartFormData takes a map with form fields and file paths and returns the encoded body and content type.
59+
// MarshalMultipartRequest handles multipart form data encoding with secure file handling and returns the encoded body and content type.
5960
func (j *JamfAPIHandler) MarshalMultipartRequest(fields map[string]string, files map[string]string, log logger.Logger) ([]byte, string, error) {
6061
body := &bytes.Buffer{}
6162
writer := multipart.NewWriter(body)
@@ -67,15 +68,16 @@ func (j *JamfAPIHandler) MarshalMultipartRequest(fields map[string]string, files
6768
}
6869
}
6970

70-
// Add the files to the form data
71-
for formField, filepath := range files {
72-
file, err := os.Open(filepath)
71+
// Add the files to the form data, using safeOpenFile to ensure secure file access
72+
for formField, filePath := range files {
73+
file, err := helpers.SafeOpenFile(filePath)
7374
if err != nil {
75+
log.Error("Failed to open file securely", zap.String("file", filePath), zap.Error(err))
7476
return nil, "", err
7577
}
7678
defer file.Close()
7779

78-
part, err := writer.CreateFormFile(formField, filepath)
80+
part, err := writer.CreateFormFile(formField, filepath.Base(filePath))
7981
if err != nil {
8082
return nil, "", err
8183
}
@@ -84,7 +86,7 @@ func (j *JamfAPIHandler) MarshalMultipartRequest(fields map[string]string, files
8486
}
8587
}
8688

87-
// Close the writer before returning
89+
// Close the writer to finish writing the multipart message
8890
contentType := writer.FormDataContentType()
8991
if err := writer.Close(); err != nil {
9092
return nil, "", err

apiintegrations/msgraph/msgraph_api_request.go

+10-7
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import (
66
"encoding/json"
77
"io"
88
"mime/multipart"
9-
"os"
9+
"path/filepath"
1010

11+
"github.com/deploymenttheory/go-api-http-client/helpers"
1112
"github.com/deploymenttheory/go-api-http-client/logger"
1213
"go.uber.org/zap"
1314
)
@@ -29,8 +30,9 @@ func (g *GraphAPIHandler) MarshalRequest(body interface{}, method string, endpoi
2930
return data, nil
3031
}
3132

32-
// MarshalMultipartFormData takes a map with form fields and file paths and returns the encoded body and content type.
33+
// MarshalMultipartRequest handles multipart form data encoding with secure file handling and returns the encoded body and content type.
3334
func (g *GraphAPIHandler) MarshalMultipartRequest(fields map[string]string, files map[string]string, log logger.Logger) ([]byte, string, error) {
35+
3436
body := &bytes.Buffer{}
3537
writer := multipart.NewWriter(body)
3638

@@ -41,15 +43,16 @@ func (g *GraphAPIHandler) MarshalMultipartRequest(fields map[string]string, file
4143
}
4244
}
4345

44-
// Add the files to the form data
45-
for formField, filepath := range files {
46-
file, err := os.Open(filepath)
46+
// Add the files to the form data, using safeOpenFile to ensure secure file access
47+
for formField, filePath := range files {
48+
file, err := helpers.SafeOpenFile(filePath)
4749
if err != nil {
50+
log.Error("Failed to open file securely", zap.String("file", filePath), zap.Error(err))
4851
return nil, "", err
4952
}
5053
defer file.Close()
5154

52-
part, err := writer.CreateFormFile(formField, filepath)
55+
part, err := writer.CreateFormFile(formField, filepath.Base(filePath))
5356
if err != nil {
5457
return nil, "", err
5558
}
@@ -58,7 +61,7 @@ func (g *GraphAPIHandler) MarshalMultipartRequest(fields map[string]string, file
5861
}
5962
}
6063

61-
// Close the writer before returning
64+
// Close the writer to finish writing the multipart message
6265
contentType := writer.FormDataContentType()
6366
if err := writer.Close(); err != nil {
6467
return nil, "", err

helpers/helpers.go

+24
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,34 @@
22
package helpers
33

44
import (
5+
"fmt"
6+
"os"
7+
"path/filepath"
58
"time"
69
)
710

811
// ParseISO8601Date attempts to parse a string date in ISO 8601 format.
912
func ParseISO8601Date(dateStr string) (time.Time, error) {
1013
return time.Parse(time.RFC3339, dateStr)
1114
}
15+
16+
// SafeOpenFile opens a file safely after validating and resolving its path.
17+
func SafeOpenFile(filePath string) (*os.File, error) {
18+
// Clean the file path to remove any ".." or similar components that can lead to directory traversal
19+
cleanPath := filepath.Clean(filePath)
20+
21+
// Resolve the clean path to an absolute path and ensure it resolves any symbolic links
22+
absPath, err := filepath.EvalSymlinks(cleanPath)
23+
if err != nil {
24+
return nil, fmt.Errorf("unable to resolve the absolute path: %s, error: %w", filePath, err)
25+
}
26+
27+
// Optionally, check if the absolute path is within a permitted directory (omitted here for brevity)
28+
// Example: allowedPathPrefix := "/safe/directory/"
29+
// if !strings.HasPrefix(absPath, allowedPathPrefix) {
30+
// return nil, fmt.Errorf("access to the file path is not allowed: %s", absPath)
31+
// }
32+
33+
// Open the file if the path is deemed safe
34+
return os.Open(absPath)
35+
}

httpclient/client_configuration.go

+23-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"log"
99
"os"
10+
"path/filepath"
1011
"strconv"
1112
"strings"
1213
"time"
@@ -24,16 +25,35 @@ const (
2425
DefaultTimeout = 10 * time.Second
2526
FollowRedirects = true
2627
MaxRedirects = 10
28+
ConfigFileExtension = ".json"
2729
)
2830

2931
// LoadConfigFromFile loads configuration values from a JSON file into the ClientConfig struct.
3032
// This function opens the specified configuration file, reads its content, and unmarshals the JSON data
3133
// into the ClientConfig struct. It's designed to initialize the client configuration with values
3234
// from a file, complementing or overriding defaults and environment variable settings.
33-
// LoadConfigFromFile loads configuration values from a JSON file into the ClientConfig struct.
3435
func LoadConfigFromFile(filePath string) (*ClientConfig, error) {
36+
// Clean up the file path to prevent directory traversal
37+
cleanPath := filepath.Clean(filePath)
38+
39+
// Resolve the cleanPath to an absolute path to ensure it resolves any symbolic links
40+
absPath, err := filepath.EvalSymlinks(cleanPath)
41+
if err != nil {
42+
return nil, fmt.Errorf("unable to resolve the absolute path of the configuration file: %s, error: %w", filePath, err)
43+
}
44+
45+
// Check for suspicious patterns in the resolved path
46+
if strings.Contains(absPath, "..") {
47+
return nil, fmt.Errorf("invalid path, path traversal patterns detected: %s", filePath)
48+
}
49+
50+
// Ensure the file has the correct extension
51+
if filepath.Ext(absPath) != ConfigFileExtension {
52+
return nil, fmt.Errorf("invalid file extension for configuration file: %s, expected .json", filePath)
53+
}
54+
3555
// Read the entire file
36-
fileBytes, err := os.ReadFile(filePath)
56+
fileBytes, err := os.ReadFile(absPath)
3757
if err != nil {
3858
return nil, fmt.Errorf("failed to read the configuration file: %s, error: %w", filePath, err)
3959
}
@@ -48,11 +68,9 @@ func LoadConfigFromFile(filePath string) (*ClientConfig, error) {
4868

4969
log.Printf("Configuration successfully loaded from file: %s", filePath)
5070

51-
// Set default values if necessary
71+
// Set default values if necessary and validate the configuration
5272
setLoggerDefaultValues(&config)
5373
setClientDefaultValues(&config)
54-
55-
// Validate mandatory configuration fields
5674
if err := validateMandatoryConfiguration(&config); err != nil {
5775
return nil, fmt.Errorf("configuration validation failed: %w", err)
5876
}

logger/zaplogger_logpath.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func EnsureLogFilePath(logPath string) (string, error) {
3131

3232
// Ensure the directory exists
3333
dir := filepath.Dir(logPath)
34-
if err := os.MkdirAll(dir, 0755); err != nil {
34+
if err := os.MkdirAll(dir, 0750); err != nil {
3535
return "", err
3636
}
3737

0 commit comments

Comments
 (0)