mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-02 09:01:59 +00:00
fix(migrations): prevent path traversal in repository restore (#38215)
## Problem The repository restorer (`services/migrations/restore.go`) builds `file://` URLs for release attachments and PR patches by joining user-supplied paths from `release.yml` and `pull_request.yml` onto the dump directory: ```go *asset.DownloadURL = "file://" + filepath.Join(r.baseDir, *asset.DownloadURL) pr.PatchURL = "file://" + filepath.Join(r.baseDir, pr.PatchURL) ``` `filepath.Join` cleans the path, so a crafted relative value such as `../../../../etc/passwd` resolves to an absolute path **outside** the dump directory. `uri.Open` then reads it via `os.Open` and stores the content as a release attachment, which is retrievable through the API — an arbitrary file read (Local File Inclusion) from a dump archive supplied to `restore-repo`. ## Fix Add a `localFileURL` helper that resolves the relative path against `baseDir` and rejects anything that escapes it. Malicious entries are skipped with a warning so a legitimate restore still completes; in-dump files keep working unchanged. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
base "gitea.dev/modules/migration"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
@@ -144,7 +145,7 @@ func (r *RepositoryRestorer) GetReleases(_ context.Context) ([]*base.Release, er
|
||||
for _, rel := range releases {
|
||||
for _, asset := range rel.Assets {
|
||||
if asset.DownloadURL != nil {
|
||||
*asset.DownloadURL = "file://" + filepath.Join(r.baseDir, *asset.DownloadURL)
|
||||
*asset.DownloadURL = "file://" + util.FilePathJoinAbs(r.baseDir, *asset.DownloadURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,7 +236,9 @@ func (r *RepositoryRestorer) GetPullRequests(_ context.Context, page, perPage in
|
||||
return nil, false, err
|
||||
}
|
||||
for _, pr := range pulls {
|
||||
pr.PatchURL = "file://" + filepath.Join(r.baseDir, pr.PatchURL)
|
||||
if pr.PatchURL != "" {
|
||||
pr.PatchURL = "file://" + util.FilePathJoinAbs(r.baseDir, pr.PatchURL)
|
||||
}
|
||||
CheckAndEnsureSafePR(pr, "", r)
|
||||
}
|
||||
return pulls, true, nil
|
||||
|
||||
47
services/migrations/restore_test.go
Normal file
47
services/migrations/restore_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/optional"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestRepositoryRestorer_GetReleases_LocalFileInclusion ensures a crafted
|
||||
// release.yml cannot make the restorer read files outside the dump
|
||||
// directory via a path-traversal DownloadURL.
|
||||
func TestRepositoryRestorer_GetReleases_LocalFileInclusion(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
|
||||
// a legitimate attachment that lives inside the dump directory
|
||||
require.NoError(t, os.WriteFile(filepath.Join(baseDir, "good.txt"), []byte("ok"), 0o644))
|
||||
|
||||
releaseYML := `
|
||||
- assets:
|
||||
- name: good.txt
|
||||
download_url: good.txt
|
||||
- name: evil.txt
|
||||
download_url: ../../../../../../../../etc/passwd
|
||||
`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(baseDir, "release.yml"), []byte(releaseYML), 0o644))
|
||||
|
||||
r, err := NewRepositoryRestorer(t.Context(), baseDir, "owner", "repo", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
releases, err := r.GetReleases(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, releases, 1)
|
||||
require.Len(t, releases[0].Assets, 2)
|
||||
|
||||
// the in-dump asset keeps a file:// URL pointing inside baseDir
|
||||
assets := releases[0].Assets
|
||||
assert.Equal(t, "file://"+filepath.Join(baseDir, "good.txt"), optional.FromPtr(assets[0].DownloadURL).Value())
|
||||
assert.Equal(t, "file://"+filepath.Join(baseDir, "etc/passwd"), optional.FromPtr(assets[1].DownloadURL).Value())
|
||||
}
|
||||
Reference in New Issue
Block a user