diff --git a/core/admin/profile.go b/core/admin/profile.go index b06dfc1e..e8e3111e 100644 --- a/core/admin/profile.go +++ b/core/admin/profile.go @@ -24,7 +24,6 @@ import ( "runtime/debug" "runtime/pprof" "strconv" - "syscall" "time" "github.com/beego/beego/v2/core/utils" @@ -66,7 +65,7 @@ func ProcessInput(input string, w io.Writer) { // MemProf record memory profile in pprof func MemProf(w io.Writer) { filename := "mem-" + strconv.Itoa(pid) + ".memprof" - f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|syscall.O_NOFOLLOW, 0600) + f, err := utils.OpenFileSecure(filename, os.O_RDWR|os.O_CREATE, 0600) if err != nil { fmt.Fprintf(w, "create file %s error %s\n", filename, err.Error()) log.Fatal("record heap profile failed: ", err) @@ -84,7 +83,7 @@ func MemProf(w io.Writer) { func GetCPUProfile(w io.Writer) { sec := 30 filename := "cpu-" + strconv.Itoa(pid) + ".pprof" - f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|syscall.O_NOFOLLOW, 0600) + f, err := utils.OpenFileSecure(filename, os.O_RDWR|os.O_CREATE, 0600) if err != nil { fmt.Fprintf(w, "Could not enable CPU profiling: %s\n", err) log.Fatal("record cpu profile failed: ", err) diff --git a/core/config/ini.go b/core/config/ini.go index 07497e83..c2d3b9e7 100644 --- a/core/config/ini.go +++ b/core/config/ini.go @@ -20,6 +20,8 @@ import ( "context" "errors" "fmt" + "github.com/beego/beego/v2/core/utils" + "github.com/mitchellh/mapstructure" "io" "os" "os/user" @@ -27,9 +29,6 @@ import ( "strconv" "strings" "sync" - "syscall" - - "github.com/mitchellh/mapstructure" ) var ( @@ -347,7 +346,7 @@ func (c *IniConfigContainer) GetSection(section string) (map[string]string, erro // BUG(env): The environment variable config item will be saved with real value in SaveConfigFile Function. func (c *IniConfigContainer) SaveConfigFile(filename string) (err error) { // Write configuration file by filename. - f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|syscall.O_NOFOLLOW, 0600) + f, err := utils.OpenFileSecure(filename, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } diff --git a/core/utils/secureopen.go b/core/utils/secureopen.go new file mode 100644 index 00000000..06b7f20c --- /dev/null +++ b/core/utils/secureopen.go @@ -0,0 +1,29 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows +// +build !windows + +package utils + +import ( + "os" + "syscall" +) + +// OpenFileSecure Secure file opening function +// Automatically add O_NOFOLLOW on Unix systems +func OpenFileSecure(name string, flag int, perm os.FileMode) (*os.File, error) { + return os.OpenFile(name, flag|syscall.O_NOFOLLOW, perm) +} diff --git a/core/utils/secureopen_windows.go b/core/utils/secureopen_windows.go new file mode 100644 index 00000000..ff443087 --- /dev/null +++ b/core/utils/secureopen_windows.go @@ -0,0 +1,47 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows +// +build windows + +package utils + +import ( + "os" + "syscall" +) + +// OpenFileSecure Secure file opening function +// Check whether it is a symbolic connection in Windows +// NOTE: This check does not prevent TOCTOU vulnerability on Windows, +// due to lack of O_NOFOLLOW or equivalent low-level open options in Go stdlib. +func OpenFileSecure(name string, flag int, perm os.FileMode) (*os.File, error) { + // Check if it's a symbolic link + if fi, err := os.Lstat(name); err == nil { + if isSymlink(fi) { + return nil, &os.PathError{ + Op: "open", + Path: name, + Err: syscall.ERROR_ACCESS_DENIED, + } + } + } + // Open the file in the normal way + return os.OpenFile(name, flag, perm) +} + +// isSymlink Check if the file is a symbolic link +func isSymlink(fi os.FileInfo) bool { + return fi.Mode()&os.ModeSymlink != 0 +} diff --git a/server/web/session/sess_file.go b/server/web/session/sess_file.go index 1a090377..0b00ebb7 100644 --- a/server/web/session/sess_file.go +++ b/server/web/session/sess_file.go @@ -18,13 +18,13 @@ import ( "context" "errors" "fmt" + "github.com/beego/beego/v2/core/utils" "io" "net/http" "os" "path/filepath" "strings" "sync" - "syscall" "time" ) @@ -100,13 +100,13 @@ func (fs *FileSessionStore) releaseSession(_ context.Context, _ http.ResponseWri _, err = os.Stat(filepath.Join(filepder.savePath, string(fs.sid[0]), string(fs.sid[1]), fs.sid)) var f *os.File if err == nil { - f, err = os.OpenFile(filepath.Join(filepder.savePath, string(fs.sid[0]), string(fs.sid[1]), fs.sid), os.O_RDWR|syscall.O_NOFOLLOW, 0o600) + f, err = utils.OpenFileSecure(filepath.Join(filepder.savePath, string(fs.sid[0]), string(fs.sid[1]), fs.sid), os.O_RDWR, 0o600) if err != nil { SLogger.Println(err) return } } else if os.IsNotExist(err) && createIfNotExist { - f, err = os.OpenFile(filepath.Join(filepder.savePath, string(fs.sid[0]), string(fs.sid[1]), fs.sid), os.O_CREATE|os.O_EXCL|os.O_RDWR|syscall.O_NOFOLLOW, 0o600) + f, err = utils.OpenFileSecure(filepath.Join(filepder.savePath, string(fs.sid[0]), string(fs.sid[1]), fs.sid), os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o600) if err != nil { SLogger.Println(err) return @@ -164,7 +164,7 @@ func (fp *FileProvider) SessionRead(ctx context.Context, sid string) (Store, err return nil, err } case os.IsNotExist(err): - f, err = os.OpenFile(sidPath, os.O_RDWR|os.O_CREATE|syscall.O_NOFOLLOW, 0600) + f, err = utils.OpenFileSecure(sidPath, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return nil, err } @@ -289,7 +289,7 @@ func (fp *FileProvider) SessionRegenerate(ctx context.Context, oldsid, sid strin } // if old sid file not exist, just create new sid file and return - newf, err := os.OpenFile(newSidFile, os.O_RDWR|os.O_CREATE|syscall.O_NOFOLLOW, 0600) + newf, err := utils.OpenFileSecure(newSidFile, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return nil, err }