mattermost/server/config/logger.go

329 lines
9.3 KiB
Go
Raw Permalink Normal View History

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"encoding/json"
"fmt"
MM-66789 Restrict log downloads to a root path for support packets (#35014) * [MM-66789] Fix arbitrary file read vulnerability in advanced logging Add path validation to prevent reading files outside the logging root directory via GetAdvancedLogs (used in support packet generation). Security controls: - Validate file paths are within logging root before reading - Support MM_LOG_PATH environment variable to allow system admins to configure a custom logging root directory - Resolve symlinks to prevent bypass attacks - Detect and block path traversal attempts Also adds: - Audit logging for support packet generation - Config-time validation that logs errors for paths outside logging root (will become blocking in future version) - Comprehensive test coverage for path validation * Update server/channels/app/platform/log_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix linter errors * Update server/channels/api4/system.go Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com> * Simplify unit tests for platform/log_test.go by moving some test logic to config/logger_test.go * Fix unit tests requiring logging root to be set * enforce LogSettings.FileLocation path validation; simplify path checking * fix linter errors * use dir in logging root for all unit test logging * MM_LOG_PATH is set once, centrally, for all tests * fix flaky test * fix flaky test --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
2026-01-29 13:29:55 -05:00
"os"
"path/filepath"
"strings"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
MM-66789 Restrict log downloads to a root path for support packets (#35014) * [MM-66789] Fix arbitrary file read vulnerability in advanced logging Add path validation to prevent reading files outside the logging root directory via GetAdvancedLogs (used in support packet generation). Security controls: - Validate file paths are within logging root before reading - Support MM_LOG_PATH environment variable to allow system admins to configure a custom logging root directory - Resolve symlinks to prevent bypass attacks - Detect and block path traversal attempts Also adds: - Audit logging for support packet generation - Config-time validation that logs errors for paths outside logging root (will become blocking in future version) - Comprehensive test coverage for path validation * Update server/channels/app/platform/log_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix linter errors * Update server/channels/api4/system.go Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com> * Simplify unit tests for platform/log_test.go by moving some test logic to config/logger_test.go * Fix unit tests requiring logging root to be set * enforce LogSettings.FileLocation path validation; simplify path checking * fix linter errors * use dir in logging root for all unit test logging * MM_LOG_PATH is set once, centrally, for all tests * fix flaky test * fix flaky test --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
2026-01-29 13:29:55 -05:00
"github.com/mattermost/mattermost/server/public/utils"
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
)
const (
LogRotateSizeMB = 100
LogCompress = true
LogRotateMaxAge = 0
LogRotateMaxBackups = 0
LogFilename = "mattermost.log"
LogMinLevelLen = 5
LogMinMsgLen = 45
LogDelim = " "
LogEnableCaller = true
)
type fileLocationFunc func(string) string
func MloggerConfigFromLoggerConfig(s *model.LogSettings, configSrc LogConfigSrc, getFileFunc fileLocationFunc) (mlog.LoggerConfiguration, error) {
cfg := make(mlog.LoggerConfiguration)
var targetCfg mlog.TargetCfg
var err error
// add the simple logging config
if *s.EnableConsole {
targetCfg, err = makeSimpleConsoleTarget(*s.ConsoleLevel, *s.ConsoleJson, *s.EnableColor)
if err != nil {
return cfg, err
}
cfg["_defConsole"] = targetCfg
}
if *s.EnableFile {
targetCfg, err = makeSimpleFileTarget(getFileFunc(*s.FileLocation), *s.FileLevel, *s.FileJson)
if err != nil {
return cfg, err
}
cfg["_defFile"] = targetCfg
}
if configSrc == nil {
return cfg, nil
}
// add advanced logging config
cfgAdv := configSrc.Get()
cfg.Append(cfgAdv)
return cfg, nil
}
func MloggerConfigFromAuditConfig(auditSettings model.ExperimentalAuditSettings, configSrc LogConfigSrc) (mlog.LoggerConfiguration, error) {
cfg := make(mlog.LoggerConfiguration)
var targetCfg mlog.TargetCfg
var err error
// add the simple audit config
if *auditSettings.FileEnabled {
targetCfg, err = makeSimpleFileTarget(*auditSettings.FileName, "error", true)
if err != nil {
return nil, err
}
// apply audit specific levels
targetCfg.Levels = []mlog.Level{mlog.LvlAuditAPI, mlog.LvlAuditContent, mlog.LvlAuditPerms, mlog.LvlAuditCLI}
// apply audit specific formatting
targetCfg.FormatOptions = json.RawMessage(`{"disable_timestamp": false, "disable_msg": true, "disable_stacktrace": true, "disable_level": true}`)
cfg["_defAudit"] = targetCfg
}
if configSrc == nil {
return cfg, nil
}
// add advanced audit config
cfgAdv := configSrc.Get()
cfg.Append(cfgAdv)
return cfg, nil
}
func GetLogFileLocation(fileLocation string) string {
if fileLocation == "" {
fileLocation, _ = fileutils.FindDir("logs")
}
return filepath.Join(fileLocation, LogFilename)
}
MM-66789 Restrict log downloads to a root path for support packets (#35014) * [MM-66789] Fix arbitrary file read vulnerability in advanced logging Add path validation to prevent reading files outside the logging root directory via GetAdvancedLogs (used in support packet generation). Security controls: - Validate file paths are within logging root before reading - Support MM_LOG_PATH environment variable to allow system admins to configure a custom logging root directory - Resolve symlinks to prevent bypass attacks - Detect and block path traversal attempts Also adds: - Audit logging for support packet generation - Config-time validation that logs errors for paths outside logging root (will become blocking in future version) - Comprehensive test coverage for path validation * Update server/channels/app/platform/log_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix linter errors * Update server/channels/api4/system.go Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com> * Simplify unit tests for platform/log_test.go by moving some test logic to config/logger_test.go * Fix unit tests requiring logging root to be set * enforce LogSettings.FileLocation path validation; simplify path checking * fix linter errors * use dir in logging root for all unit test logging * MM_LOG_PATH is set once, centrally, for all tests * fix flaky test * fix flaky test --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
2026-01-29 13:29:55 -05:00
// 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 {
ci: enable fullyparallel mode for server tests (#35816) * ci: enable fullyparallel mode for server tests Replace os.Setenv, os.Chdir, and global state mutations with parallel-safe alternatives (t.Setenv, t.Chdir, test hooks) across 37 files. Refactor GetLogRootPath and MM_INSTALL_TYPE to use package-level test hooks instead of environment variables. This enables gotestsum --fullparallel, allowing all test packages to run with maximum parallelism within each shard. Co-authored-by: Claude <claude@anthropic.com> * ci: split fullyparallel from continue-on-error in workflow template - Add new boolean input 'allow-failure' separate from 'fullyparallel' - Change continue-on-error to use allow-failure instead of fullyparallel - Update server-ci.yml to pass allow-failure: true for test coverage job - Allows independent control of parallel execution and failure tolerance Co-authored-by: Claude <claude@anthropic.com> * fix: protect TestOverrideLogRootPath with sync.Mutex for parallel tests - Replace global var TestOverrideLogRootPath with mutex-protected functions - Add SetTestOverrideLogRootPath() and getTestOverrideLogRootPath() functions - Update GetLogRootPath() to use thread-safe getter - Update all test files to use SetTestOverrideLogRootPath() with t.Cleanup() - Fixes race condition when running tests with t.Parallel() Co-authored-by: Claude <claude@anthropic.com> * fix: configure audit settings before server setup in tests - Move ExperimentalAuditSettings from UpdateConfig() to config defaults - Pass audit config via app.Config() option in SetupWithServerOptions() - Fixes audit test setup ordering to configure BEFORE server initialization - Resolves CodeRabbit's audit config timing issue in api4 tests Co-authored-by: Claude <claude@anthropic.com> * fix: implement SetTestOverrideLogRootPath mutex in logger.go The previous commit updated test callers to use SetTestOverrideLogRootPath() but didn't actually create the function in config/logger.go, causing build failures across all CI shards. This commit: - Replaces the exported var TestOverrideLogRootPath with mutex-protected unexported state (testOverrideLogRootPath + testOverrideLogRootMu) - Adds exported SetTestOverrideLogRootPath() setter - Adds unexported getTestOverrideLogRootPath() getter - Updates GetLogRootPath() to use the thread-safe getter - Fixes log_test.go callers that were missed in the previous commit Co-authored-by: Claude <claude@anthropic.com> * fix(test): use SetupConfig for access_control feature flag registration InitAccessControlPolicy() checks FeatureFlags.AttributeBasedAccessControl at route registration time during server startup. Setting the flag via UpdateConfig after Setup() is too late — routes are never registered and API calls return 404. Use SetupConfig() to pass the feature flag in the initial config before server startup, ensuring routes are properly registered. Co-authored-by: Claude <claude@anthropic.com> * fix(test): restore BurnOnRead flag state in TestRevealPost subtest The 'feature not enabled' subtest disables BurnOnRead without restoring it via t.Cleanup. Subsequent subtests inherit the disabled state, which can cause 501 errors when they expect the feature to be available. Add t.Cleanup to restore FeatureFlags.BurnOnRead = true after the subtest completes. Co-authored-by: Claude <claude@anthropic.com> * fix(test): restore EnableSharedChannelsMemberSync flag via t.Cleanup The test disables EnableSharedChannelsMemberSync without restoring it. If the subtest exits early (e.g., require failure), later sibling subtests inherit a disabled flag and become flaky. Add t.Cleanup to restore the flag after the subtest completes. Co-authored-by: Claude <claude@anthropic.com> * Fix test parallelism: use instance-scoped overrides and init-time audit config Replace package-level test globals (TestOverrideInstallType, SetTestOverrideLogRootPath) with fields on PlatformService so each test gets its own instance without process-wide mutation. Fix three audit tests (TestUserLoginAudit, TestLogoutAuditAuthStatus, TestUpdatePasswordAudit) that configured the audit logger after server init — the audit logger only reads config at startup, so pass audit settings via app.Config() at init time instead. Also revert the Go 1.24.13 downgrade and bump mattermost-govet to v2.0.2 for Go 1.25.8 compatibility. * Fix audit unit tests * Fix MMCLOUDURL unit tests * Fixed unit tests using MM_NOTIFY_ADMIN_COOL_OFF_DAYS * Make app migrations idempotent for parallel test safety Change System().Save() to System().SaveOrUpdate() in all migration completion markers. When two parallel tests share a database pool entry, both may race through the check-then-insert migration pattern. Save() causes a duplicate key fatal crash; SaveOrUpdate() makes the second write a harmless no-op. * test: address review feedback on fullyparallel PR - Use SetLogRootPathOverride() setter instead of direct field access in platform/support_packet_test.go and platform/log_test.go (pvev) - Restore TestGetLogRootPath in config/logger_test.go to keep MM_LOG_PATH env var coverage; test uses t.Setenv so it runs serially which is fine (pvev) - Fix misleading comment in config_test.go: code uses t.Setenv, not os.Setenv (jgheithcock) Co-authored-by: Claude <claude@anthropic.com> * fix: add missing os import in post_test.go The os import was dropped during a merge conflict resolution while burn-on-read shared channel tests from master still use os.Setenv. Co-authored-by: Claude <claude@anthropic.com> --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: wiggin77 <wiggin77@warpmail.net> Co-authored-by: Mattermost Build <build@mattermost.com>
2026-04-08 20:48:36 -04:00
// Check environment variable
MM-66789 Restrict log downloads to a root path for support packets (#35014) * [MM-66789] Fix arbitrary file read vulnerability in advanced logging Add path validation to prevent reading files outside the logging root directory via GetAdvancedLogs (used in support packet generation). Security controls: - Validate file paths are within logging root before reading - Support MM_LOG_PATH environment variable to allow system admins to configure a custom logging root directory - Resolve symlinks to prevent bypass attacks - Detect and block path traversal attempts Also adds: - Audit logging for support packet generation - Config-time validation that logs errors for paths outside logging root (will become blocking in future version) - Comprehensive test coverage for path validation * Update server/channels/app/platform/log_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix linter errors * Update server/channels/api4/system.go Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com> * Simplify unit tests for platform/log_test.go by moving some test logic to config/logger_test.go * Fix unit tests requiring logging root to be set * enforce LogSettings.FileLocation path validation; simplify path checking * fix linter errors * use dir in logging root for all unit test logging * MM_LOG_PATH is set once, centrally, for all tests * fix flaky test * fix flaky test --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
2026-01-29 13:29:55 -05:00
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 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 {
return mlog.TargetCfg{}, err
}
target := mlog.TargetCfg{
Type: "console",
Levels: levels,
Options: json.RawMessage(`{"out": "stdout"}`),
MaxQueueSize: 1000,
}
if outputJSON {
target.Format = "json"
target.FormatOptions = makeJSONFormatOptions()
} else {
target.Format = "plain"
target.FormatOptions = makePlainFormatOptions(color)
}
return target, nil
}
func makeSimpleFileTarget(filename string, level string, json bool) (mlog.TargetCfg, error) {
levels, err := stdLevels(level)
if err != nil {
return mlog.TargetCfg{}, err
}
fileOpts, err := makeFileOptions(filename)
if err != nil {
return mlog.TargetCfg{}, fmt.Errorf("cannot encode file options: %w", err)
}
target := mlog.TargetCfg{
Type: "file",
Levels: levels,
Options: fileOpts,
MaxQueueSize: 1000,
}
if json {
target.Format = "json"
target.FormatOptions = makeJSONFormatOptions()
} else {
target.Format = "plain"
target.FormatOptions = makePlainFormatOptions(false)
}
return target, nil
}
func stdLevels(level string) ([]mlog.Level, error) {
stdLevel, err := stringToStdLevel(level)
if err != nil {
return nil, err
}
var levels []mlog.Level
for _, l := range mlog.StdAll {
if l.ID <= stdLevel.ID {
levels = append(levels, l)
}
}
return levels, nil
}
func stringToStdLevel(level string) (mlog.Level, error) {
level = strings.ToLower(level)
for _, l := range mlog.StdAll {
if l.Name == level {
return l, nil
}
}
return mlog.Level{}, fmt.Errorf("%s is not a standard level", level)
}
func makeJSONFormatOptions() json.RawMessage {
str := fmt.Sprintf(`{"enable_caller": %t}`, LogEnableCaller)
return json.RawMessage(str)
}
func makePlainFormatOptions(enableColor bool) json.RawMessage {
str := fmt.Sprintf(`{"delim": "%s", "min_level_len": %d, "min_msg_len": %d, "enable_color": %t, "enable_caller": %t}`,
LogDelim, LogMinLevelLen, LogMinMsgLen, enableColor, LogEnableCaller)
return json.RawMessage(str)
}
func makeFileOptions(filename string) (json.RawMessage, error) {
opts := struct {
Filename string `json:"filename"`
Max_size int `json:"max_size"`
Max_age int `json:"max_age"`
Max_backups int `json:"max_backups"`
Compress bool `json:"compress"`
}{
Filename: filename,
Max_size: LogRotateSizeMB,
Max_age: LogRotateMaxAge,
Max_backups: LogRotateMaxBackups,
Compress: LogCompress,
}
b, err := json.Marshal(opts)
if err != nil {
return nil, err
}
return json.RawMessage(b), nil
}