VYPR
High severityCISA KEVNVD Advisory· Published Dec 10, 2025· Updated Feb 26, 2026

File overwrite in file update API in Gogs

CVE-2025-8110

Description

Improper Symbolic link handling in the PutContents API in Gogs allows Local Execution of Code.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
gogs.io/gogsGo
<= 0.13.3

Affected products

1

Patches

1
553707f3fd5f

repository: reject any updates that has symlink in path hierarchy (#8082)

https://github.com/gogs/gogsᴊᴏᴇ ᴄʜᴇɴJan 8, 2026via ghsa
2 files changed · +61 62
  • internal/database/repo_editor.go+56 53 modified
    @@ -107,6 +107,19 @@ func (r *Repository) CheckoutNewBranch(oldBranch, newBranch string) error {
     	return nil
     }
     
    +// hasSymlinkInPath returns true if there is any symlink in path hierarchy using
    +// the given base and relative path.
    +func hasSymlinkInPath(base, relPath string) bool {
    +	parts := strings.Split(filepath.ToSlash(relPath), "/")
    +	for i := range parts {
    +		filePath := path.Join(append([]string{base}, parts[:i+1]...)...)
    +		if osutil.IsSymlink(filePath) {
    +			return true
    +		}
    +	}
    +	return false
    +}
    +
     type UpdateRepoFileOptions struct {
     	OldBranch   string
     	NewBranch   string
    @@ -118,7 +131,7 @@ type UpdateRepoFileOptions struct {
     }
     
     // UpdateRepoFile adds or updates a file in repository.
    -func (r *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (err error) {
    +func (r *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) error {
     	// 🚨 SECURITY: Prevent uploading files into the ".git" directory.
     	if isRepositoryGitPath(opts.NewTreeName) {
     		return errors.Errorf("bad tree path %q", opts.NewTreeName)
    @@ -127,15 +140,21 @@ func (r *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (err
     	repoWorkingPool.CheckIn(com.ToStr(r.ID))
     	defer repoWorkingPool.CheckOut(com.ToStr(r.ID))
     
    -	if err = r.DiscardLocalRepoBranchChanges(opts.OldBranch); err != nil {
    -		return fmt.Errorf("discard local r branch[%s] changes: %v", opts.OldBranch, err)
    +	if err := r.DiscardLocalRepoBranchChanges(opts.OldBranch); err != nil {
    +		return fmt.Errorf("discard local repo branch[%s] changes: %v", opts.OldBranch, err)
     	} else if err = r.UpdateLocalCopyBranch(opts.OldBranch); err != nil {
     		return fmt.Errorf("update local copy branch[%s]: %v", opts.OldBranch, err)
     	}
     
    -	repoPath := r.RepoPath()
     	localPath := r.LocalCopyPath()
     
    +	// 🚨 SECURITY: Prevent touching files in surprising places, reject operations
    +	// that involve symlinks.
    +	if hasSymlinkInPath(localPath, opts.OldTreeName) || hasSymlinkInPath(localPath, opts.NewTreeName) {
    +		return errors.New("cannot update file with symbolic link in path")
    +	}
    +
    +	repoPath := r.RepoPath()
     	if opts.OldBranch != opts.NewBranch {
     		// Directly return error if new branch already exists in the server
     		if git.RepoHasBranch(repoPath, opts.NewBranch) {
    @@ -144,7 +163,7 @@ func (r *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (err
     
     		// Otherwise, delete branch from local copy in case out of sync
     		if git.RepoHasBranch(localPath, opts.NewBranch) {
    -			if err = git.DeleteBranch(localPath, opts.NewBranch, git.DeleteBranchOptions{
    +			if err := git.DeleteBranch(localPath, opts.NewBranch, git.DeleteBranchOptions{
     				Force: true,
     			}); err != nil {
     				return fmt.Errorf("delete branch %q: %v", opts.NewBranch, err)
    @@ -157,46 +176,31 @@ func (r *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (err
     	}
     
     	oldFilePath := path.Join(localPath, opts.OldTreeName)
    -	filePath := path.Join(localPath, opts.NewTreeName)
    -	if err = os.MkdirAll(path.Dir(filePath), os.ModePerm); err != nil {
    -		return err
    +	newFilePath := path.Join(localPath, opts.NewTreeName)
    +
    +	// Prompt the user if the meant-to-be new file already exists.
    +	if osutil.IsExist(newFilePath) && opts.IsNewFile {
    +		return ErrRepoFileAlreadyExist{newFilePath}
     	}
     
    -	// If it's meant to be a new file, make sure it doesn't exist.
    -	if opts.IsNewFile {
    -		// 🚨 SECURITY: Prevent updating files in surprising place, check if the file is
    -		// a symlink.
    -		if osutil.IsSymlink(filePath) {
    -			return fmt.Errorf("cannot update symbolic link: %s", opts.NewTreeName)
    -		}
    -		if osutil.IsExist(filePath) {
    -			return ErrRepoFileAlreadyExist{filePath}
    -		}
    +	if err := os.MkdirAll(path.Dir(newFilePath), os.ModePerm); err != nil {
    +		return errors.Wrapf(err, "create parent directories of %q", newFilePath)
     	}
     
    -	// Ignore move step if it's a new file under a directory.
    -	// Otherwise, move the file when name changed.
     	if osutil.IsFile(oldFilePath) && opts.OldTreeName != opts.NewTreeName {
    -		// 🚨 SECURITY: Prevent updating files in surprising place, check if the file is
    -		// a symlink.
    -		if osutil.IsSymlink(oldFilePath) {
    -			return fmt.Errorf("cannot move symbolic link: %s", opts.OldTreeName)
    -		}
    -
    -		if err = git.Move(localPath, opts.OldTreeName, opts.NewTreeName); err != nil {
    -			return fmt.Errorf("git mv %q %q: %v", opts.OldTreeName, opts.NewTreeName, err)
    +		if err := git.Move(localPath, opts.OldTreeName, opts.NewTreeName); err != nil {
    +			return errors.Wrapf(err, "git mv %q %q", opts.OldTreeName, opts.NewTreeName)
     		}
     	}
     
    -	if err = os.WriteFile(filePath, []byte(opts.Content), 0600); err != nil {
    +	if err := os.WriteFile(newFilePath, []byte(opts.Content), 0o600); err != nil {
     		return fmt.Errorf("write file: %v", err)
     	}
     
    -	if err = git.Add(localPath, git.AddOptions{All: true}); err != nil {
    +	if err := git.Add(localPath, git.AddOptions{All: true}); err != nil {
     		return fmt.Errorf("git add --all: %v", err)
     	}
    -
    -	err = git.CreateCommit(
    +	err := git.CreateCommit(
     		localPath,
     		&git.Signature{
     			Name:  doer.DisplayName(),
    @@ -230,7 +234,7 @@ func (r *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (err
     }
     
     // GetDiffPreview produces and returns diff result of a file which is not yet committed.
    -func (r *Repository) GetDiffPreview(branch, treePath, content string) (diff *gitutil.Diff, err error) {
    +func (r *Repository) GetDiffPreview(branch, treePath, content string) (*gitutil.Diff, error) {
     	// 🚨 SECURITY: Prevent uploading files into the ".git" directory.
     	if isRepositoryGitPath(treePath) {
     		return nil, errors.Errorf("bad tree path %q", treePath)
    @@ -239,23 +243,24 @@ func (r *Repository) GetDiffPreview(branch, treePath, content string) (diff *git
     	repoWorkingPool.CheckIn(com.ToStr(r.ID))
     	defer repoWorkingPool.CheckOut(com.ToStr(r.ID))
     
    -	if err = r.DiscardLocalRepoBranchChanges(branch); err != nil {
    -		return nil, fmt.Errorf("discard local r branch[%s] changes: %v", branch, err)
    +	if err := r.DiscardLocalRepoBranchChanges(branch); err != nil {
    +		return nil, fmt.Errorf("discard local repo branch[%s] changes: %v", branch, err)
     	} else if err = r.UpdateLocalCopyBranch(branch); err != nil {
     		return nil, fmt.Errorf("update local copy branch[%s]: %v", branch, err)
     	}
     
     	localPath := r.LocalCopyPath()
     	filePath := path.Join(localPath, treePath)
     
    -	// 🚨 SECURITY: Prevent updating files in surprising place, check if the target is
    -	// a symlink.
    -	if osutil.IsSymlink(filePath) {
    -		return nil, fmt.Errorf("cannot get diff preview for symbolic link: %s", treePath)
    +	// 🚨 SECURITY: Prevent touching files in surprising places, reject operations
    +	// that involve symlinks.
    +	if hasSymlinkInPath(localPath, treePath) {
    +		return nil, errors.New("cannot update file with symbolic link in path")
     	}
    -	if err = os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
    -		return nil, err
    -	} else if err = os.WriteFile(filePath, []byte(content), 0600); err != nil {
    +
    +	if err := os.MkdirAll(path.Dir(filePath), os.ModePerm); err != nil {
    +		return nil, errors.Wrap(err, "create parent directories")
    +	} else if err = os.WriteFile(filePath, []byte(content), 0o600); err != nil {
     		return nil, fmt.Errorf("write file: %v", err)
     	}
     
    @@ -276,15 +281,13 @@ func (r *Repository) GetDiffPreview(branch, treePath, content string) (diff *git
     	pid := process.Add(fmt.Sprintf("GetDiffPreview [repo_path: %s]", r.RepoPath()), cmd)
     	defer process.Remove(pid)
     
    -	diff, err = gitutil.ParseDiff(stdout, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars)
    +	diff, err := gitutil.ParseDiff(stdout, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars)
     	if err != nil {
     		return nil, fmt.Errorf("parse diff: %v", err)
     	}
    -
     	if err = cmd.Wait(); err != nil {
     		return nil, fmt.Errorf("wait: %v", err)
     	}
    -
     	return diff, nil
     }
     
    @@ -319,21 +322,21 @@ func (r *Repository) DeleteRepoFile(doer *User, opts DeleteRepoFileOptions) (err
     		return fmt.Errorf("update local copy branch[%s]: %v", opts.OldBranch, err)
     	}
     
    +	localPath := r.LocalCopyPath()
    +
    +	// 🚨 SECURITY: Prevent touching files in surprising places, reject operations
    +	// that involve symlinks.
    +	if hasSymlinkInPath(localPath, opts.TreePath) {
    +		return errors.New("cannot update file with symbolic link in path")
    +	}
    +
     	if opts.OldBranch != opts.NewBranch {
     		if err := r.CheckoutNewBranch(opts.OldBranch, opts.NewBranch); err != nil {
     			return fmt.Errorf("checkout new branch[%s] from old branch[%s]: %v", opts.NewBranch, opts.OldBranch, err)
     		}
     	}
     
    -	localPath := r.LocalCopyPath()
     	filePath := path.Join(localPath, opts.TreePath)
    -
    -	// 🚨 SECURITY: Prevent updating files in surprising place, check if the file is
    -	// a symlink.
    -	if osutil.IsSymlink(filePath) {
    -		return fmt.Errorf("cannot delete symbolic link: %s", opts.TreePath)
    -	}
    -
     	if err = os.Remove(filePath); err != nil {
     		return fmt.Errorf("remove file %q: %v", opts.TreePath, err)
     	}
    
  • internal/osutil/osutil.go+5 9 modified
    @@ -9,7 +9,8 @@ import (
     	"os/user"
     )
     
    -// IsFile returns true if given path exists as a file (i.e. not a directory).
    +// IsFile returns true if given path exists as a file (i.e. not a directory)
    +// following any symlinks.
     func IsFile(path string) bool {
     	f, e := os.Stat(path)
     	if e != nil {
    @@ -18,8 +19,8 @@ func IsFile(path string) bool {
     	return !f.IsDir()
     }
     
    -// IsDir returns true if given path is a directory, and returns false when it's
    -// a file or does not exist.
    +// IsDir returns true if given path is a directory following any symlinks, and
    +// returns false when it's a file or does not exist.
     func IsDir(dir string) bool {
     	f, e := os.Stat(dir)
     	if e != nil {
    @@ -28,23 +29,18 @@ func IsDir(dir string) bool {
     	return f.IsDir()
     }
     
    -// IsExist returns true if a file or directory exists.
    +// IsExist returns true if a file or directory exists following any symlinks.
     func IsExist(path string) bool {
     	_, err := os.Stat(path)
     	return err == nil || os.IsExist(err)
     }
     
     // IsSymlink returns true if given path is a symbolic link.
     func IsSymlink(path string) bool {
    -	if !IsExist(path) {
    -		return false
    -	}
    -
     	fileInfo, err := os.Lstat(path)
     	if err != nil {
     		return false
     	}
    -
     	return fileInfo.Mode()&os.ModeSymlink != 0
     }
     
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

12

News mentions

0

No linked articles in our index yet.