Skip to content

Commit c904afa

Browse files
authored
Fix isAllowedDir matching by prefix (#1276)
* harden isAllowedDir * fix lint issue * simplify check to direct matches or subdirectories * simplify check * recursively check parent directory when determining if a path is allowed * add test for allowing root directory * remove logs * fix relative paths in benchmark test * fails config apply when deleting a file outside allowed dirs, update manifest UT * fix fieldalignment
1 parent ec0cb2a commit c904afa

File tree

8 files changed

+96
-50
lines changed

8 files changed

+96
-50
lines changed

internal/config/config_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,8 +1037,8 @@ func agentConfig() *Config {
10371037
},
10381038
},
10391039
AllowedDirectories: []string{
1040-
"/etc/nginx/", "/etc/nginx-agent/", "/usr/local/etc/nginx/", "/var/run/nginx/", "/var/log/nginx/",
1041-
"/usr/share/nginx/modules/", "/etc/app_protect/",
1040+
"/etc/nginx", "/etc/nginx-agent", "/usr/local/etc/nginx", "/var/run/nginx", "/var/log/nginx",
1041+
"/usr/share/nginx/modules", "/etc/app_protect",
10421042
},
10431043
Collector: createDefaultCollectorConfig(),
10441044
Command: &Command{

internal/config/types.go

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import (
99
"context"
1010
"errors"
1111
"fmt"
12-
"log/slog"
1312
"path/filepath"
14-
"regexp"
13+
"slices"
1514
"strings"
1615
"time"
1716

@@ -356,10 +355,7 @@ func (col *Collector) Validate(allowedDirectories []string) error {
356355
var err error
357356
cleanedConfPath := filepath.Clean(col.ConfigPath)
358357

359-
allowed, err := isAllowedDir(cleanedConfPath, allowedDirectories)
360-
if err != nil {
361-
return err
362-
}
358+
allowed := isAllowedDir(cleanedConfPath, allowedDirectories)
363359
if !allowed {
364360
err = errors.Join(err, fmt.Errorf("collector path %s not allowed", col.ConfigPath))
365361
}
@@ -378,10 +374,7 @@ func (nr *NginxReceiver) Validate(allowedDirectories []string) error {
378374
}
379375

380376
for _, al := range nr.AccessLogs {
381-
allowed, allowedError := isAllowedDir(al.FilePath, allowedDirectories)
382-
if allowedError != nil {
383-
err = errors.Join(err, fmt.Errorf("invalid nginx receiver access log path: %s", al.FilePath))
384-
}
377+
allowed := isAllowedDir(al.FilePath, allowedDirectories)
385378
if !allowed {
386379
err = errors.Join(err, fmt.Errorf("nginx receiver access log path %s not allowed", al.FilePath))
387380
}
@@ -396,13 +389,9 @@ func (nr *NginxReceiver) Validate(allowedDirectories []string) error {
396389
return err
397390
}
398391

399-
func (c *Config) IsDirectoryAllowed(directory string) bool {
400-
allow, err := isAllowedDir(directory, c.AllowedDirectories)
401-
if err != nil {
402-
slog.Warn("Unable to determine if directory is allowed", "error", err)
403-
return false
404-
}
405-
392+
// IsAllowedDirectory checks if the given path is in the list of allowed directories.
393+
func (c *Config) IsDirectoryAllowed(path string) bool {
394+
allow := isAllowedDir(path, c.AllowedDirectories)
406395
return allow
407396
}
408397

@@ -480,32 +469,19 @@ func (c *Config) IsCommandServerProxyConfigured() bool {
480469
}
481470

482471
// isAllowedDir checks if the given path is in the list of allowed directories.
483-
// It returns true if the path is allowed, false otherwise.
484-
// If the path is allowed but does not exist, it also logs a warning.
485-
// It also checks if the path is a file, in which case it checks the parent directory of the file.
486-
func isAllowedDir(path string, allowedDirs []string) (bool, error) {
487-
if len(allowedDirs) == 0 {
488-
return false, errors.New("no allowed directories configured")
489-
}
490-
491-
directoryPath := path
492-
// Check if the path is a file, regex matches when end of string is /<filename>.<extension>
493-
isFilePath, err := regexp.MatchString(`/(\w+)\.(\w+)$`, directoryPath)
494-
if err != nil {
495-
return false, errors.New("error matching path" + directoryPath)
496-
}
472+
// It recursively checks the parent directories of the path, until it finds a match or reaches the root directory.
473+
func isAllowedDir(path string, allowedDirs []string) bool {
474+
return checkDirIsAllowed(filepath.Clean(path), allowedDirs)
475+
}
497476

498-
if isFilePath {
499-
directoryPath = filepath.Dir(directoryPath)
477+
func checkDirIsAllowed(path string, allowedDirs []string) bool {
478+
if slices.Contains(allowedDirs, path) {
479+
return true
500480
}
501481

502-
for _, allowedDirectory := range allowedDirs {
503-
// Check if the directoryPath starts with the allowedDirectory
504-
// This allows for subdirectories within the allowed directories.
505-
if strings.HasPrefix(directoryPath, allowedDirectory) {
506-
return true, nil
507-
}
482+
if path == "/" || !strings.HasPrefix(path, "/") { // root directory reached with no match, path is not allowed
483+
return false
508484
}
509485

510-
return false, nil
486+
return checkDirIsAllowed(filepath.Dir(path), allowedDirs)
511487
}

internal/config/types_test.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,69 @@ func TestTypes_isAllowedDir(t *testing.T) {
6666
},
6767
filePath: "/not-nginx-test/idontexist.conf",
6868
},
69+
{
70+
name: "Test 7: Prefix match not allowed",
71+
allowed: false,
72+
allowedDirs: []string{
73+
"/etc/nginx",
74+
},
75+
filePath: "/etc/nginx-test/nginx.conf",
76+
},
77+
{
78+
name: "Test 8: Empty allowed directories",
79+
allowed: false,
80+
allowedDirs: []string{
81+
"",
82+
},
83+
filePath: "/etc/nginx/nginx.conf",
84+
},
85+
{
86+
name: "Test 9: Multiple allowed directories, file in one of them",
87+
allowed: true,
88+
allowedDirs: []string{
89+
"/etc/nginx",
90+
"/usr/local/nginx",
91+
},
92+
filePath: "/usr/local/nginx/nginx.conf",
93+
},
94+
{
95+
name: "Test 10: Multiple allowed directories, file not in any of them",
96+
allowed: false,
97+
allowedDirs: []string{
98+
"/etc/nginx",
99+
"/usr/local/nginx",
100+
},
101+
filePath: "/opt/nginx/nginx.conf",
102+
},
103+
{
104+
name: "Test 11: Path is root directory, not in allowed directories",
105+
allowed: false,
106+
allowedDirs: []string{
107+
"/etc/nginx",
108+
},
109+
filePath: "/",
110+
},
111+
{
112+
name: "Test 12: File is in directory nested 5 levels deep within allowed directory",
113+
allowed: true,
114+
allowedDirs: []string{
115+
"/etc/nginx",
116+
},
117+
filePath: "/etc/nginx/level1/level2/level3/level4/nginx.conf",
118+
},
119+
{
120+
name: "Test 13: Root directory is allowed, file in subdirectory",
121+
allowed: true,
122+
allowedDirs: []string{
123+
"/",
124+
},
125+
filePath: "/etc/nginx/nginx.conf",
126+
},
69127
}
70128

71129
for _, test := range tests {
72130
t.Run(test.name, func(t *testing.T) {
73-
result, err := isAllowedDir(test.filePath, test.allowedDirs)
74-
require.NoError(t, err)
131+
result := isAllowedDir(test.filePath, test.allowedDirs)
75132
require.Equal(t, test.allowed, result)
76133
})
77134
}

internal/datasource/config/nginx_config_parser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func NewNginxConfigParser(agentConfig *config.Config) *NginxConfigParser {
7474
}
7575

7676
func (ncp *NginxConfigParser) Parse(ctx context.Context, instance *mpi.Instance) (*model.NginxConfigContext, error) {
77-
configPath := instance.GetInstanceRuntime().GetConfigPath()
77+
configPath, _ := filepath.Abs(instance.GetInstanceRuntime().GetConfigPath())
7878

7979
if !ncp.agentConfig.IsDirectoryAllowed(configPath) {
8080
return nil, fmt.Errorf("config path %s is not in allowed directories", configPath)

internal/datasource/config/nginx_config_parser_benchmark_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ func BenchmarkNginxConfigParser_Parse(b *testing.B) {
3535
for _, configFilePath := range configFilePaths {
3636
func(configFilePath string) {
3737
b.Run(configFilePath, func(bb *testing.B) {
38+
absPath, _ := filepath.Abs(configFilePath)
3839
agentConfig.AllowedDirectories = []string{
39-
filepath.Dir(configFilePath),
40+
filepath.Dir(absPath),
4041
}
4142

4243
nginxConfigParser := NewNginxConfigParser(

internal/file/file_manager_service.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,12 @@ func (fms *FileManagerService) DetermineFileActions(
344344
}
345345
fileContents[fileName] = fileContent
346346

347+
// Allowed directories could have been modified since file was created,
348+
// so we should check before marking for deletion
349+
if !fms.agentConfig.IsDirectoryAllowed(fileName) {
350+
return nil, nil, fmt.Errorf("error deleting file %s: file not in allowed directories", fileName)
351+
}
352+
347353
fileDiff[fileName] = &model.FileCache{
348354
File: manifestFile,
349355
Action: model.Delete,

internal/file/file_manager_service_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ func TestFileManagerService_Rollback(t *testing.T) {
462462

463463
func TestFileManagerService_DetermineFileActions(t *testing.T) {
464464
ctx := context.Background()
465-
tempDir := os.TempDir()
465+
tempDir := filepath.Clean(os.TempDir())
466466

467467
deleteTestFile := helpers.CreateFileWithErrorCheck(t, tempDir, "nginx_delete.conf")
468468
defer helpers.RemoveFileWithErrorCheck(t, deleteTestFile.Name())
@@ -498,9 +498,11 @@ func TestFileManagerService_DetermineFileActions(t *testing.T) {
498498
expectedCache map[string]*model.FileCache
499499
expectedContent map[string][]byte
500500
name string
501+
allowedDirs []string
501502
}{
502503
{
503-
name: "Test 1: Add, Update & Delete Files",
504+
name: "Test 1: Add, Update & Delete Files",
505+
allowedDirs: []string{tempDir},
504506
modifiedFiles: map[string]*model.FileCache{
505507
addTestFileName: {
506508
File: &mpi.File{
@@ -563,7 +565,8 @@ func TestFileManagerService_DetermineFileActions(t *testing.T) {
563565
expectedError: nil,
564566
},
565567
{
566-
name: "Test 2: Files same as on disk",
568+
name: "Test 2: Files same as on disk",
569+
allowedDirs: []string{tempDir},
567570
modifiedFiles: map[string]*model.FileCache{
568571
addTestFile.Name(): {
569572
File: &mpi.File{
@@ -598,6 +601,7 @@ func TestFileManagerService_DetermineFileActions(t *testing.T) {
598601
},
599602
{
600603
name: "Test 3: File being deleted already doesn't exist",
604+
allowedDirs: []string{tempDir},
601605
modifiedFiles: make(map[string]*model.FileCache),
602606
currentFiles: map[string]*mpi.File{
603607
"/unknown/file.conf": {
@@ -620,6 +624,7 @@ func TestFileManagerService_DetermineFileActions(t *testing.T) {
620624

621625
fakeFileServiceClient := &v1fakes.FakeFileServiceClient{}
622626
fileManagerService := NewFileManagerService(fakeFileServiceClient, types.AgentConfig(), &sync.RWMutex{})
627+
fileManagerService.agentConfig.AllowedDirectories = test.allowedDirs
623628
fileManagerService.agentConfig.LibDir = manifestDirPath
624629
fileManagerService.manifestFilePath = manifestFilePath
625630

@@ -794,6 +799,7 @@ func TestFileManagerService_UpdateManifestFile(t *testing.T) {
794799

795800
fakeFileServiceClient := &v1fakes.FakeFileServiceClient{}
796801
fileManagerService := NewFileManagerService(fakeFileServiceClient, types.AgentConfig(), &sync.RWMutex{})
802+
fileManagerService.agentConfig.AllowedDirectories = []string{"manifestDirPath"}
797803
fileManagerService.agentConfig.LibDir = manifestDirPath
798804
fileManagerService.manifestFilePath = file.Name()
799805

test/types/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func AgentConfig() *config.Config {
6262
Multiplier: commonMultiplier,
6363
},
6464
},
65-
AllowedDirectories: []string{"/tmp/"},
65+
AllowedDirectories: []string{"/tmp"},
6666
Collector: &config.Collector{
6767
ConfigPath: "/etc/nginx-agent/nginx-agent-otelcol.yaml",
6868
Exporters: config.Exporters{

0 commit comments

Comments
 (0)