Cherry-pick MM-66789: Restrict log downloads to a root path for support packets (#35164)

Automatic Merge
Этот коммит содержится в:
Doug Lauder
2026-02-02 14:23:28 -05:00
коммит произвёл GitHub
родитель d2594e5046
Коммит 463f7a0511
13 изменённых файлов: 1050 добавлений и 7 удалений

Просмотреть файл

@@ -6,11 +6,13 @@ package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/utils"
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
)
@@ -127,6 +129,123 @@ func GetLogSettingsFromNotificationsLogSettings(notificationLogSettings *model.N
return settings
}
// GetLogRootPath returns the root directory for all log files.
// This is used for security validation to prevent arbitrary file reads via advanced logging.
// The logging root is determined by:
// 1. MM_LOG_PATH environment variable (if set and non-empty)
// 2. The default "logs" directory (found relative to the binary)
func GetLogRootPath() string {
// Check environment variable first
if envPath := os.Getenv("MM_LOG_PATH"); envPath != "" {
absPath, err := filepath.Abs(envPath)
if err == nil {
return absPath
}
}
// Fall back to default logs directory
logsDir, _ := fileutils.FindDir("logs")
absPath, err := filepath.Abs(logsDir)
if err != nil {
return logsDir
}
return absPath
}
// ValidateLogFilePath validates that a log file path is within the logging root directory.
// This prevents arbitrary file read/write vulnerabilities in logging configuration.
// The logging root is determined by MM_LOG_PATH environment variable or the configured log directory.
func ValidateLogFilePath(filePath string, loggingRoot string) error {
// Resolve file path to absolute
absPath, err := filepath.Abs(filePath)
if err != nil {
return fmt.Errorf("cannot resolve path %s: %w", filePath, err)
}
// Resolve symlinks to prevent bypass via symlink attacks
realPath, err := filepath.EvalSymlinks(absPath)
if err != nil {
// If file doesn't exist, still validate the intended path
if !os.IsNotExist(err) {
return fmt.Errorf("cannot resolve symlinks for %s: %w", absPath, err)
}
} else {
absPath = realPath
}
// Resolve logging root to absolute
absRoot, err := filepath.Abs(loggingRoot)
if err != nil {
return fmt.Errorf("cannot resolve logging root %s: %w", loggingRoot, err)
}
// Ensure root has trailing separator for proper prefix matching
// This prevents /tmp/log matching /tmp/logger
rootWithSep := absRoot
if !strings.HasSuffix(rootWithSep, string(filepath.Separator)) {
rootWithSep += string(filepath.Separator)
}
// Check if file is within the logging root
// Allow exact match (absPath == absRoot) or proper prefix match
if absPath != absRoot && !strings.HasPrefix(absPath, rootWithSep) {
return fmt.Errorf("path %s is outside logging root %s", filePath, absRoot)
}
return nil
}
// WarnIfLogPathsOutsideRoot validates log file paths in the config and logs errors for paths outside the logging root.
// This is called during config save to identify configurations that will cause server startup to fail in a future version.
// Currently only logs errors; in a future version this will block server startup.
func WarnIfLogPathsOutsideRoot(cfg *model.Config) {
loggingRoot := GetLogRootPath()
// Check LogSettings.AdvancedLoggingJSON
if !utils.IsEmptyJSON(cfg.LogSettings.AdvancedLoggingJSON) {
validateAdvancedLoggingConfig(cfg.LogSettings.AdvancedLoggingJSON, "LogSettings.AdvancedLoggingJSON", loggingRoot)
}
// Check NotificationLogSettings.AdvancedLoggingJSON
if !utils.IsEmptyJSON(cfg.NotificationLogSettings.AdvancedLoggingJSON) {
validateAdvancedLoggingConfig(cfg.NotificationLogSettings.AdvancedLoggingJSON, "NotificationLogSettings.AdvancedLoggingJSON", loggingRoot)
}
// Check ExperimentalAuditSettings.AdvancedLoggingJSON
if !utils.IsEmptyJSON(cfg.ExperimentalAuditSettings.AdvancedLoggingJSON) {
validateAdvancedLoggingConfig(cfg.ExperimentalAuditSettings.AdvancedLoggingJSON, "ExperimentalAuditSettings.AdvancedLoggingJSON", loggingRoot)
}
}
func validateAdvancedLoggingConfig(loggingJSON json.RawMessage, configName string, loggingRoot string) {
logCfg := make(mlog.LoggerConfiguration)
if err := json.Unmarshal(loggingJSON, &logCfg); err != nil {
return
}
for targetName, target := range logCfg {
if target.Type != "file" {
continue
}
var fileOption struct {
Filename string `json:"filename"`
}
if err := json.Unmarshal(target.Options, &fileOption); err != nil {
continue
}
if err := ValidateLogFilePath(fileOption.Filename, loggingRoot); err != nil {
mlog.Error("Log file path in logging config is outside logging root directory. This configuration will cause server startup to fail in a future version. To fix, set MM_LOG_PATH environment variable to a parent directory containing all log paths, or move log files to the configured logging root.",
mlog.String("config_section", configName),
mlog.String("target", targetName),
mlog.String("path", fileOption.Filename),
mlog.String("logging_root", loggingRoot),
mlog.Err(err))
}
}
}
func makeSimpleConsoleTarget(level string, outputJSON bool, color bool) (mlog.TargetCfg, error) {
levels, err := stdLevels(level)
if err != nil {

Просмотреть файл

@@ -5,6 +5,8 @@ package config
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@@ -50,3 +52,169 @@ func TestMloggerConfigFromAuditConfig(t *testing.T) {
assert.Equal(t, optionsExpected, optionsReceived)
})
}
func TestGetLogRootPath(t *testing.T) {
t.Run("returns MM_LOG_PATH when set", func(t *testing.T) {
// Create a temp directory to use as MM_LOG_PATH
dir, err := os.MkdirTemp("", "logroot")
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(dir)
})
t.Setenv("MM_LOG_PATH", dir)
result := GetLogRootPath()
absDir, _ := filepath.Abs(dir)
assert.Equal(t, absDir, result)
})
t.Run("finds logs directory relative to binary when MM_LOG_PATH not set", func(t *testing.T) {
// When MM_LOG_PATH is not set, GetLogRootPath falls back to FindDir("logs"),
// which searches for a "logs" directory relative to the working directory
// and the binary location. Create a logs directory relative to the test
// binary to verify this behavior.
t.Setenv("MM_LOG_PATH", "")
// Get the test binary location
exe, err := os.Executable()
require.NoError(t, err)
exe, err = filepath.EvalSymlinks(exe)
require.NoError(t, err)
binaryDir := filepath.Dir(exe)
// Create a "logs" directory next to the binary
logsDir := filepath.Join(binaryDir, "logs")
err = os.MkdirAll(logsDir, 0755)
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(logsDir)
})
result := GetLogRootPath()
// Result should be an absolute path
assert.True(t, filepath.IsAbs(result), "GetLogRootPath should return an absolute path, got: %s", result)
// FindDir searches working directory first, then binary directory.
// The result should be either the logs directory we created or another
// logs directory found earlier in the search path. Either way, it should
// be a valid directory path ending in "logs".
assert.True(t, filepath.Base(result) == "logs" || result == "./",
"GetLogRootPath should return a logs directory path, got: %s", result)
})
}
func TestValidateLogFilePath(t *testing.T) {
t.Run("valid path within root", func(t *testing.T) {
root, err := os.MkdirTemp("", "logroot")
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(root)
})
validFile := filepath.Join(root, "app.log")
err = os.WriteFile(validFile, []byte("test"), 0644)
require.NoError(t, err)
err = ValidateLogFilePath(validFile, root)
assert.NoError(t, err)
})
t.Run("valid path in subdirectory", func(t *testing.T) {
root, err := os.MkdirTemp("", "logroot")
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(root)
})
subdir := filepath.Join(root, "subdir")
err = os.MkdirAll(subdir, 0755)
require.NoError(t, err)
validFile := filepath.Join(subdir, "app.log")
err = os.WriteFile(validFile, []byte("test"), 0644)
require.NoError(t, err)
err = ValidateLogFilePath(validFile, root)
assert.NoError(t, err)
})
t.Run("rejects absolute path outside root", func(t *testing.T) {
root, err := os.MkdirTemp("", "logroot")
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(root)
})
outsideDir, err := os.MkdirTemp("", "outside")
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(outsideDir)
})
outsideFile := filepath.Join(outsideDir, "secret.txt")
err = os.WriteFile(outsideFile, []byte("secret"), 0644)
require.NoError(t, err)
err = ValidateLogFilePath(outsideFile, root)
assert.Error(t, err)
assert.Contains(t, err.Error(), "outside logging root")
})
t.Run("rejects path traversal attack", func(t *testing.T) {
root, err := os.MkdirTemp("", "logroot")
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(root)
})
traversalPath := filepath.Join(root, "..", "..", "etc", "passwd")
err = ValidateLogFilePath(traversalPath, root)
assert.Error(t, err)
assert.Contains(t, err.Error(), "outside logging root")
})
t.Run("rejects symlink pointing outside root", func(t *testing.T) {
root, err := os.MkdirTemp("", "logroot")
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(root)
})
outsideDir, err := os.MkdirTemp("", "outside")
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(outsideDir)
})
// Create a file outside the root
outsideFile := filepath.Join(outsideDir, "secret.txt")
err = os.WriteFile(outsideFile, []byte("secret"), 0644)
require.NoError(t, err)
// Create a symlink inside root pointing to the outside file
symlinkPath := filepath.Join(root, "sneaky.log")
err = os.Symlink(outsideFile, symlinkPath)
require.NoError(t, err)
err = ValidateLogFilePath(symlinkPath, root)
assert.Error(t, err)
assert.Contains(t, err.Error(), "outside logging root")
})
t.Run("allows non-existent file path within root", func(t *testing.T) {
root, err := os.MkdirTemp("", "logroot")
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(root)
})
// File doesn't exist but path is within root
nonExistentFile := filepath.Join(root, "future.log")
err = ValidateLogFilePath(nonExistentFile, root)
assert.NoError(t, err)
})
}