mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-03 10:50:54 +02:00
## 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>
48 lines
1.5 KiB
Go
48 lines
1.5 KiB
Go
// 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())
|
|
}
|