Skip to content

Add validation to allowed directories on config load #1144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 40 additions & 21 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"log/slog"
"os"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
Expand All @@ -36,7 +37,11 @@ const (
EnvPrefix = "NGINX_AGENT"
KeyDelimiter = "_"
KeyValueNumber = 2
AgentDirName = "/etc/nginx-agent/"
AgentDirName = "/etc/nginx-agent"

// Regular expression to match invalid characters in paths.
// It matches whitespace, control characters, non-printable characters, and specific Unicode characters.
regexInvalidPath = "\\s|[[:cntrl:]]|[[:space:]]|[[^:print:]]|ㅤ|\\.\\.|\\*"
)

var viperInstance = viper.NewWithOptions(viper.KeyDelimiter(KeyDelimiter))
Expand Down Expand Up @@ -79,27 +84,13 @@ func RegisterConfigFile() error {
}

func ResolveConfig() (*Config, error) {
// Collect allowed directories, so that paths in the config can be validated.
directories := viperInstance.GetStringSlice(AllowedDirectoriesKey)
allowedDirs := []string{AgentDirName}

log := resolveLog()
slogger := logger.New(log.Path, log.Level)
slog.SetDefault(slogger)

// Check directories in allowed_directories are valid
for _, dir := range directories {
if dir == "" || !filepath.IsAbs(dir) {
slog.Warn("Invalid directory: ", "dir", dir)
continue
}

if !strings.HasSuffix(dir, "/") {
dir += "/"
}
allowedDirs = append(allowedDirs, dir)
}

// Collect allowed directories, so that paths in the config can be validated.
directories := viperInstance.GetStringSlice(AllowedDirectoriesKey)
allowedDirs := resolveAllowedDirectories(directories)
slog.Info("Configured allowed directories", "allowed_directories", allowedDirs)

// Collect all parsing errors before returning the error, so the user sees all issues with config
Expand Down Expand Up @@ -129,13 +120,40 @@ func ResolveConfig() (*Config, error) {

checkCollectorConfiguration(collector, config)

slog.Info(
"Excluded files from being watched for file changes",
"exclude_files",
config.Watchers.FileWatcher.ExcludeFiles,
)

slog.Debug("Agent config", "config", config)
slog.Info("Excluded files from being watched for file changes", "exclude_files",
config.Watchers.FileWatcher.ExcludeFiles)

return config, nil
}

// resolveAllowedDirectories checks if the provided directories are valid and returns a slice of cleaned absolute paths.
// It ignores empty paths, paths that are not absolute, and paths containing invalid characters.
// Invalid paths are logged as warnings.
func resolveAllowedDirectories(dirs []string) []string {
allowed := []string{AgentDirName}
for _, dir := range dirs {
re := regexp.MustCompile(regexInvalidPath)
invalidChars := re.MatchString(dir)
if dir == "" || dir == "/" || !filepath.IsAbs(dir) || invalidChars {
slog.Warn("Ignoring invalid directory", "dir", dir)
continue
}
dir = filepath.Clean(dir)
if dir == AgentDirName {
// If the directory is the default agent directory, we skip adding it again.
continue
}
allowed = append(allowed, dir)
}

return allowed
}

func checkCollectorConfiguration(collector *Collector, config *Config) {
if isOTelExporterConfigured(collector) && config.IsGrpcClientConfigured() && config.IsAuthConfigured() &&
config.IsTLSConfigured() {
Expand Down Expand Up @@ -744,13 +762,14 @@ func resolveClient() *Client {
}

func resolveCollector(allowedDirs []string) (*Collector, error) {
// Collect receiver configurations
var receivers Receivers

err := resolveMapStructure(CollectorReceiversKey, &receivers)
if err != nil {
return nil, fmt.Errorf("unmarshal collector receivers config: %w", err)
}

// Collect exporter configurations
exporters, err := resolveExporters()
if err != nil {
return nil, fmt.Errorf("unmarshal collector exporters config: %w", err)
Expand Down
256 changes: 173 additions & 83 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,84 @@ func TestNormalizeFunc(t *testing.T) {
assert.Equal(t, expected, result)
}

func TestResolveAllowedDirectories(t *testing.T) {
tests := []struct {
name string
configuredDirs []string
expected []string
}{
{
name: "Empty path",
configuredDirs: []string{""},
expected: []string{"/etc/nginx-agent"},
},
{
name: "Absolute path",
configuredDirs: []string{"/etc/agent/"},
expected: []string{"/etc/nginx-agent", "/etc/agent"},
},
{
name: "Absolute paths",
configuredDirs: []string{"/etc/nginx/"},
expected: []string{"/etc/nginx-agent", "/etc/nginx"},
},
{
name: "Absolute path with multiple slashes",
configuredDirs: []string{"/etc///////////nginx-agent/"},
expected: []string{"/etc/nginx-agent"},
},
{
name: "Absolute path with directory traversal",
configuredDirs: []string{"/etc/nginx/../nginx-agent"},
expected: []string{"/etc/nginx-agent"},
},
{
name: "Absolute path with repeat directory traversal",
configuredDirs: []string{"/etc/nginx-agent/../../../../../nginx-agent"},
expected: []string{"/etc/nginx-agent"},
},
{
name: "Absolute path with control characters",
configuredDirs: []string{"/etc/nginx-agent/\\x08../tmp/"},
expected: []string{"/etc/nginx-agent"},
},
{
name: "Absolute path with invisible characters",
configuredDirs: []string{"/etc/nginx-agent/ㅤㅤㅤ/tmp/"},
expected: []string{"/etc/nginx-agent"},
},
{
name: "Absolute path with escaped invisible characters",
configuredDirs: []string{"/etc/nginx-agent/\\\\ㅤ/tmp/"},
expected: []string{"/etc/nginx-agent"},
},
{
name: "Mixed paths",
configuredDirs: []string{
"nginx-agent",
"",
"..",
"/",
"\\/",
".",
"/etc/nginx/",
},
expected: []string{"/etc/nginx-agent", "/etc/nginx"},
},
{
name: "Relative path",
configuredDirs: []string{"nginx-agent"},
expected: []string{"/etc/nginx-agent"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
allowed := resolveAllowedDirectories(test.configuredDirs)
assert.Equal(t, test.expected, allowed)
})
}
}

func TestResolveLog(t *testing.T) {
viperInstance = viper.NewWithOptions(viper.KeyDelimiter(KeyDelimiter))
viperInstance.Set(LogLevelKey, "error")
Expand Down Expand Up @@ -764,7 +842,7 @@ func agentConfig() *Config {
return &Config{
UUID: "",
Version: "",
Path: "",
Path: "testdata/agent.conf",
Log: &Log{},
Client: &Client{
HTTP: &HTTP{
Expand All @@ -789,87 +867,14 @@ func agentConfig() *Config {
},
},
AllowedDirectories: []string{
"/etc/nginx/", "/etc/nginx-agent/", "/usr/local/etc/nginx/", "/var/run/nginx/", "/var/log/nginx/",
"/usr/share/nginx/modules/",
},
Collector: &Collector{
ConfigPath: "/etc/nginx-agent/nginx-agent-otelcol.yaml",
Exporters: Exporters{
OtlpExporters: []OtlpExporter{
{
Server: &ServerConfig{
Host: "127.0.0.1",
Port: 1234,
Type: Grpc,
},
TLS: &TLSConfig{
Cert: "/path/to/server-cert.pem",
Key: "/path/to/server-cert.pem",
Ca: "/path/to/server-cert.pem",
SkipVerify: true,
ServerName: "remote-saas-server",
},
},
},
},
Processors: Processors{
Batch: &Batch{
SendBatchMaxSize: DefCollectorBatchProcessorSendBatchMaxSize,
SendBatchSize: DefCollectorBatchProcessorSendBatchSize,
Timeout: DefCollectorBatchProcessorTimeout,
},
},
Receivers: Receivers{
OtlpReceivers: []OtlpReceiver{
{
Server: &ServerConfig{
Host: "localhost",
Port: 4317,
Type: Grpc,
},
Auth: &AuthConfig{
Token: "even-secreter-token",
},
OtlpTLSConfig: &OtlpTLSConfig{
GenerateSelfSignedCert: false,
Cert: "/path/to/server-cert.pem",
Key: "/path/to/server-cert.pem",
Ca: "/path/to/server-cert.pem",
SkipVerify: true,
ServerName: "local-data-plane-server",
},
},
},
NginxReceivers: []NginxReceiver{
{
InstanceID: "cd7b8911-c2c5-4daf-b311-dbead151d938",
StubStatus: APIDetails{
URL: "http://localhost:4321/status",
Listen: "",
},
AccessLogs: []AccessLog{
{
LogFormat: accessLogFormat,
FilePath: "/var/log/nginx/access-custom.conf",
},
},
},
},
},
Extensions: Extensions{
Health: &Health{
Server: &ServerConfig{
Host: "localhost",
Port: 1337,
},
Path: "/",
},
},
Log: &Log{
Level: "INFO",
Path: "/var/log/nginx-agent/opentelemetry-collector-agent.log",
},
"/etc/nginx",
"/etc/nginx-agent",
"/usr/local/etc/nginx",
"/var/run/nginx",
"/var/log/nginx",
"/usr/share/nginx/modules",
},
Collector: createDefaultCollectorConfig(),
Command: &Command{
Server: &ServerConfig{
Host: "127.0.0.1",
Expand Down Expand Up @@ -922,8 +927,12 @@ func createConfig() *Config {
},
},
AllowedDirectories: []string{
"/etc/nginx-agent/", "/etc/nginx/", "/usr/local/etc/nginx/", "/var/run/nginx/",
"/usr/share/nginx/modules/", "/var/log/nginx/",
"/etc/nginx-agent",
"/etc/nginx",
"/usr/local/etc/nginx",
"/var/run/nginx",
"/usr/share/nginx/modules",
"/var/log/nginx",
},
DataPlaneConfig: &DataPlaneConfig{
Nginx: &NginxDataPlaneConfig{
Expand Down Expand Up @@ -1105,3 +1114,84 @@ func createConfig() *Config {
},
}
}

func createDefaultCollectorConfig() *Collector {
return &Collector{
ConfigPath: "/etc/nginx-agent/testdata/nginx-agent-otelcol.yaml",
Exporters: Exporters{
OtlpExporters: []OtlpExporter{
{
Server: &ServerConfig{
Host: "127.0.0.1",
Port: 1234,
Type: Grpc,
},
TLS: &TLSConfig{
Cert: "/path/to/server-cert.pem",
Key: "/path/to/server-cert.pem",
Ca: "/path/to/server-cert.pem",
SkipVerify: true,
ServerName: "remote-saas-server",
},
},
},
},
Processors: Processors{
Batch: &Batch{
SendBatchMaxSize: DefCollectorBatchProcessorSendBatchMaxSize,
SendBatchSize: DefCollectorBatchProcessorSendBatchSize,
Timeout: DefCollectorBatchProcessorTimeout,
},
},
Receivers: Receivers{
OtlpReceivers: []OtlpReceiver{
{
Server: &ServerConfig{
Host: "localhost",
Port: 4317,
Type: Grpc,
},
Auth: &AuthConfig{
Token: "even-secreter-token",
},
OtlpTLSConfig: &OtlpTLSConfig{
GenerateSelfSignedCert: false,
Cert: "/path/to/server-cert.pem",
Key: "/path/to/server-cert.pem",
Ca: "/path/to/server-cert.pem",
SkipVerify: true,
ServerName: "local-data-plane-server",
},
},
},
NginxReceivers: []NginxReceiver{
{
InstanceID: "cd7b8911-c2c5-4daf-b311-dbead151d938",
StubStatus: APIDetails{
URL: "http://localhost:4321/status",
Listen: "",
},
AccessLogs: []AccessLog{
{
LogFormat: accessLogFormat,
FilePath: "/var/log/nginx/access-custom.conf",
},
},
},
},
},
Extensions: Extensions{
Health: &Health{
Server: &ServerConfig{
Host: "localhost",
Port: 1337,
},
Path: "/",
},
},
Log: &Log{
Level: "INFO",
Path: "/var/log/nginx-agent/opentelemetry-collector-agent.log",
},
}
}
Loading