Skip to content

Commit bfbee64

Browse files
committed
fix: enforced maximum size for environment varaiable
1 parent 5533fc1 commit bfbee64

File tree

5 files changed

+308
-272
lines changed

5 files changed

+308
-272
lines changed

env_config.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@ package instana
66
import (
77
"errors"
88
"fmt"
9+
"os"
10+
"path/filepath"
911
"strconv"
1012
"strings"
1113
"time"
14+
15+
"github.com/stretchr/testify/assert/yaml"
1216
)
1317

18+
// MaxEnvValueSize is the maximum size of the value of an environment variable.
19+
const MaxEnvValueSize = 32 * 1024
20+
1421
// parseInstanaTags parses the tags string passed via INSTANA_TAGS.
1522
// The tag string is a comma-separated list of keys optionally followed by an '=' character and a string value:
1623
//
@@ -107,3 +114,148 @@ func parseInstanaTimeout(s string) (time.Duration, error) {
107114

108115
return time.Duration(ms) * time.Millisecond, nil
109116
}
117+
118+
// parseInstanaTracingDisable processes the INSTANA_TRACING_DISABLE environment variable value
119+
// and updates the TracerOptions.Disable map accordingly.
120+
//
121+
// When the value is a boolean (true/false), the whole tracing feature is disabled/enabled.
122+
// When a list of category or type names is specified, only those will be disabled.
123+
//
124+
// Examples:
125+
// INSTANA_TRACING_DISABLE=true - disables all tracing
126+
// INSTANA_TRACING_DISABLE="logging" - disables logging category
127+
func parseInstanaTracingDisable(value string, opts *TracerOptions) {
128+
// Initialize the Disable map if it doesn't exist
129+
if opts.DisableSpans == nil {
130+
opts.DisableSpans = make(map[string]bool)
131+
}
132+
133+
// Trim spaces from the value
134+
value = strings.TrimSpace(value)
135+
136+
// if it's a boolean value, disable all categories
137+
if strings.EqualFold(value, "true") {
138+
opts.DisableAllCategories()
139+
return
140+
}
141+
142+
// if it's not a boolean value, process as a comma-separated list and disable each category.
143+
items := strings.Split(value, ",")
144+
for _, item := range items {
145+
item = strings.TrimSpace(item)
146+
if item != "" {
147+
opts.DisableSpans[item] = true
148+
}
149+
}
150+
}
151+
152+
// parseConfigFile reads and parses the YAML configuration file at the given path
153+
// and updates the TracerOptions accordingly.
154+
//
155+
// The YAML file must follow this format:
156+
// tracing:
157+
// disable:
158+
// - logging: true
159+
160+
func parseConfigFile(path string, opts *TracerOptions) error {
161+
// Validate the file path and security considerations
162+
absPath, err := validateFile(path)
163+
if err != nil {
164+
return fmt.Errorf("config file validation failed for %s: %w", path, err)
165+
}
166+
167+
// Read the file with proper error handling
168+
data, err := os.ReadFile(absPath)
169+
if err != nil {
170+
return fmt.Errorf("failed to read config file: %w", err)
171+
}
172+
173+
type Config struct {
174+
Tracing struct {
175+
Disable []map[string]bool `yaml:"disable"`
176+
} `yaml:"tracing"`
177+
}
178+
179+
var config Config
180+
if err := yaml.Unmarshal(data, &config); err != nil {
181+
return fmt.Errorf("failed to parse YAML: %w", err)
182+
}
183+
184+
if opts.DisableSpans == nil {
185+
opts.DisableSpans = make(map[string]bool)
186+
}
187+
188+
// Add the categories configured in the YAML file to the Disable map
189+
for _, disableMap := range config.Tracing.Disable {
190+
for category, enabled := range disableMap {
191+
if enabled {
192+
opts.DisableSpans[category] = true
193+
}
194+
}
195+
196+
}
197+
198+
return nil
199+
}
200+
201+
// validateFile ensures the given config file path is safe and usable.
202+
// Security considerations:
203+
// - Resolves symlinks to prevent symlink attacks
204+
// - Ensures the path exists and is a regular file
205+
// - Enforces a reasonable file size limit to avoid DoS
206+
// - Warns if file permissions are too permissive (world-readable)
207+
func validateFile(path string) (absPath string, err error) {
208+
// Resolve symlinks to avoid symlink attacks
209+
realPath, err := filepath.EvalSymlinks(path)
210+
if err != nil {
211+
return absPath, fmt.Errorf("failed to resolve config file path: %w", err)
212+
}
213+
214+
// Get absolute normalized path
215+
absPath, err = filepath.Abs(realPath)
216+
if err != nil {
217+
return absPath, fmt.Errorf("failed to get absolute path: %w", err)
218+
}
219+
220+
// Check if the path exists and is a regular file
221+
fileInfo, err := os.Stat(absPath)
222+
if err != nil {
223+
return absPath, fmt.Errorf("failed to access config file: %w", err)
224+
}
225+
226+
// Ensure it's a regular file, not a directory or special file
227+
if !fileInfo.Mode().IsRegular() {
228+
return absPath, fmt.Errorf("config path is not a regular file: %s", absPath)
229+
}
230+
231+
// Enforce a maximum file size
232+
const maxFileSize = 1 * 1024 * 1024 // 1MB
233+
if fileInfo.Size() > maxFileSize {
234+
return absPath, fmt.Errorf("config file too large: %d bytes (max allowed: %d bytes)",
235+
fileInfo.Size(), maxFileSize)
236+
}
237+
238+
// Warn if the file is world-readable (optional hardening)
239+
if fileInfo.Mode().Perm()&0004 != 0 {
240+
defaultLogger.Warn("config file is world-readable, consider restricting permissions: ", absPath)
241+
}
242+
243+
return absPath, nil
244+
}
245+
246+
// LookupValidatedEnv retrieves the value of the environment variable named by key.
247+
// It validates if env value exceeds the configured MaxEnvValueSize limit.
248+
// On success, it returns the variable's value.
249+
func lookupValidatedEnv(key string) (string, bool) {
250+
envVal, ok := os.LookupEnv(key)
251+
if !ok {
252+
return "", false
253+
}
254+
255+
if len(envVal) > MaxEnvValueSize {
256+
defaultLogger.Error(fmt.Errorf("value of %q exceeds safe limit (%d bytes)", key, MaxEnvValueSize))
257+
return "", false
258+
}
259+
260+
return envVal, true
261+
}

env_config_internal_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ package instana
55

66
import (
77
"errors"
8+
"fmt"
9+
"os"
10+
"path/filepath"
811
"regexp"
12+
"strings"
913
"testing"
1014
"time"
1115

@@ -146,3 +150,134 @@ func TestParseInstanaTimeout_Error(t *testing.T) {
146150
})
147151
}
148152
}
153+
154+
func TestValidateFile(t *testing.T) {
155+
tempDir := t.TempDir()
156+
157+
validFilePath := filepath.Join(tempDir, "valid.txt")
158+
err := os.WriteFile(validFilePath, []byte("test content"), 0644)
159+
if err != nil {
160+
t.Fatalf("Failed to create test file: %v", err)
161+
}
162+
163+
tests := []struct {
164+
name string
165+
getPathFn func() (string, error)
166+
expectedError bool
167+
errorContains string
168+
}{
169+
{
170+
name: "Valid file",
171+
getPathFn: func() (string, error) {
172+
return validFilePath, nil
173+
},
174+
expectedError: false,
175+
},
176+
{
177+
name: "Non-existent file",
178+
getPathFn: func() (string, error) {
179+
return filepath.Join(tempDir, "nonexistent.txt"), nil
180+
},
181+
expectedError: true,
182+
errorContains: "no such file or directory",
183+
},
184+
{
185+
name: "Symlink to valid file",
186+
getPathFn: func() (string, error) {
187+
symlinkPath := filepath.Join(tempDir, "symlink.txt")
188+
err = os.Symlink(validFilePath, symlinkPath)
189+
if err != nil {
190+
return "", fmt.Errorf("Skipping symlink test, could not create symlink: %v", err)
191+
}
192+
return symlinkPath, nil
193+
},
194+
expectedError: false,
195+
},
196+
{
197+
name: "Directory instead of file",
198+
getPathFn: func() (string, error) {
199+
dirPath := filepath.Join(tempDir, "testdir")
200+
err = os.Mkdir(dirPath, 0755)
201+
if err != nil {
202+
return "", fmt.Errorf("Failed to create test directory: %v", err)
203+
}
204+
return dirPath, nil
205+
},
206+
expectedError: true,
207+
errorContains: "not a regular file",
208+
},
209+
{
210+
name: "File too large",
211+
getPathFn: func() (string, error) {
212+
fpath := filepath.Join(tempDir, "big.conf")
213+
f, err := os.Create(fpath)
214+
if err != nil {
215+
return "", fmt.Errorf("Failed to create test file: %v", err)
216+
}
217+
defer f.Close()
218+
err = f.Truncate(1024*1024 + 1) // >1MB
219+
if err != nil {
220+
return "", fmt.Errorf("Failed to truncate test file: %v", err)
221+
}
222+
return fpath, nil
223+
},
224+
expectedError: true,
225+
errorContains: "config file too large",
226+
},
227+
{
228+
name: "World-readable file",
229+
getPathFn: func() (string, error) {
230+
worldReadablePath := filepath.Join(tempDir, "world-readable.txt")
231+
err = os.WriteFile(worldReadablePath, []byte("world-readable content"), 0644)
232+
if err != nil {
233+
return "", fmt.Errorf("Failed to create world-readable test file: %v", err)
234+
}
235+
return worldReadablePath, nil
236+
},
237+
expectedError: false, // This should not error, but will log a warning
238+
},
239+
}
240+
241+
for _, tt := range tests {
242+
t.Run(tt.name, func(t *testing.T) {
243+
path, err := tt.getPathFn()
244+
if err != nil {
245+
t.Skip(err)
246+
}
247+
absPath, err := validateFile(path)
248+
249+
if (err != nil) != tt.expectedError {
250+
if tt.expectedError {
251+
t.Errorf("Expected error but got none")
252+
} else {
253+
t.Errorf("Expected no error but got: %v", err)
254+
}
255+
}
256+
257+
// If am error is expected, check that it contains the expected text
258+
if tt.expectedError && err != nil && tt.errorContains != "" {
259+
if !strings.Contains(err.Error(), tt.errorContains) {
260+
t.Errorf("Error message '%s' does not contain '%s'", err.Error(), tt.errorContains)
261+
}
262+
}
263+
264+
// For successful cases, check that the returned path is absolute
265+
if !tt.expectedError && err == nil {
266+
if !filepath.IsAbs(absPath) {
267+
t.Errorf("Expected absolute path, got: %s", absPath)
268+
}
269+
270+
// For the symlink case, verify it was resolved
271+
if tt.name == "Symlink to valid file" {
272+
// The resolved path should be an absolute path to the target file
273+
// Note: On macOS, /var/folders may resolve to /private/var/folders
274+
// so just check that the base filename matches
275+
if filepath.Base(absPath) != filepath.Base(validFilePath) {
276+
t.Errorf("Symlink not properly resolved. Got: %s, Expected file with basename: %s",
277+
absPath, filepath.Base(validFilePath))
278+
}
279+
}
280+
}
281+
})
282+
}
283+
}

0 commit comments

Comments
 (0)