Skip to content

Commit 328a0b4

Browse files
committed
[v11.0/forgejo] fix: prevent writing to out-of-repo symlink destinations while evaluating template repos
1 parent 1808b48 commit 328a0b4

File tree

5 files changed

+444
-122
lines changed

5 files changed

+444
-122
lines changed

services/repository/files/update.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type ChangeRepoFile struct {
4343
ContentReader io.ReadSeeker
4444
SHA string
4545
Options *RepoFileOptions
46+
Symlink bool
4647
}
4748

4849
// ChangeRepoFilesOptions holds the repository files update options
@@ -62,6 +63,7 @@ type RepoFileOptions struct {
6263
treePath string
6364
fromTreePath string
6465
executable bool
66+
symlink bool
6567
}
6668

6769
// ChangeRepoFiles adds, updates or removes multiple files in the given repository
@@ -116,6 +118,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
116118
treePath: treePath,
117119
fromTreePath: fromTreePath,
118120
executable: false,
121+
symlink: file.Symlink,
119122
}
120123
treePaths = append(treePaths, treePath)
121124
}
@@ -421,14 +424,14 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file
421424
}
422425

423426
// Add the object to the index
427+
mode := "100644" // regular file
424428
if file.Options.executable {
425-
if err := t.AddObjectToIndex("100755", objectHash, file.Options.treePath); err != nil {
426-
return err
427-
}
428-
} else {
429-
if err := t.AddObjectToIndex("100644", objectHash, file.Options.treePath); err != nil {
430-
return err
431-
}
429+
mode = "100755"
430+
} else if file.Options.symlink {
431+
mode = "120644"
432+
}
433+
if err := t.AddObjectToIndex(mode, objectHash, file.Options.treePath); err != nil {
434+
return err
432435
}
433436

434437
if lfsMetaObject != nil {

services/repository/generate.go

Lines changed: 0 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"context"
1010
"fmt"
1111
"os"
12-
"path"
1312
"path/filepath"
1413
"regexp"
1514
"strconv"
@@ -154,114 +153,6 @@ func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
154153
}, nil
155154
}
156155

157-
func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error {
158-
commitTimeStr := time.Now().Format(time.RFC3339)
159-
authorSig := repo.Owner.NewGitSig()
160-
161-
// Because this may call hooks we should pass in the environment
162-
env := append(os.Environ(),
163-
"GIT_AUTHOR_NAME="+authorSig.Name,
164-
"GIT_AUTHOR_EMAIL="+authorSig.Email,
165-
"GIT_AUTHOR_DATE="+commitTimeStr,
166-
"GIT_COMMITTER_NAME="+authorSig.Name,
167-
"GIT_COMMITTER_EMAIL="+authorSig.Email,
168-
"GIT_COMMITTER_DATE="+commitTimeStr,
169-
)
170-
171-
// Clone to temporary path and do the init commit.
172-
templateRepoPath := templateRepo.RepoPath()
173-
if err := git.Clone(ctx, templateRepoPath, tmpDir, git.CloneRepoOptions{
174-
Depth: 1,
175-
Branch: templateRepo.DefaultBranch,
176-
}); err != nil {
177-
return fmt.Errorf("git clone: %w", err)
178-
}
179-
180-
if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil {
181-
return fmt.Errorf("remove git dir: %w", err)
182-
}
183-
184-
// Variable expansion
185-
gt, err := checkGiteaTemplate(tmpDir)
186-
if err != nil {
187-
return fmt.Errorf("checkGiteaTemplate: %w", err)
188-
}
189-
190-
if gt != nil {
191-
if err := util.Remove(gt.Path); err != nil {
192-
return fmt.Errorf("remove .giteatemplate: %w", err)
193-
}
194-
195-
// Avoid walking tree if there are no globs
196-
if len(gt.Globs()) > 0 {
197-
tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
198-
if err := filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
199-
if walkErr != nil {
200-
return walkErr
201-
}
202-
203-
if d.IsDir() {
204-
return nil
205-
}
206-
207-
base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
208-
for _, g := range gt.Globs() {
209-
if g.Match(base) {
210-
content, err := os.ReadFile(path)
211-
if err != nil {
212-
return err
213-
}
214-
215-
if err := os.WriteFile(path,
216-
[]byte(generateExpansion(string(content), templateRepo, generateRepo, false)),
217-
0o644); err != nil {
218-
return err
219-
}
220-
221-
substPath := filepath.FromSlash(filepath.Join(tmpDirSlash,
222-
generateExpansion(base, templateRepo, generateRepo, true)))
223-
224-
// Create parent subdirectories if needed or continue silently if it exists
225-
if err := os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
226-
return err
227-
}
228-
229-
// Substitute filename variables
230-
if err := os.Rename(path, substPath); err != nil {
231-
return err
232-
}
233-
234-
break
235-
}
236-
}
237-
return nil
238-
}); err != nil {
239-
return err
240-
}
241-
}
242-
}
243-
244-
if err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
245-
return err
246-
}
247-
248-
repoPath := repo.RepoPath()
249-
if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repoPath).
250-
SetDescription(fmt.Sprintf("generateRepoCommit (git remote add): %s to %s", templateRepoPath, tmpDir)).
251-
RunStdString(&git.RunOpts{Dir: tmpDir, Env: env}); err != nil {
252-
log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err)
253-
return fmt.Errorf("git remote add: %w", err)
254-
}
255-
256-
// set default branch based on whether it's specified in the newly generated repo or not
257-
defaultBranch := repo.DefaultBranch
258-
if strings.TrimSpace(defaultBranch) == "" {
259-
defaultBranch = templateRepo.DefaultBranch
260-
}
261-
262-
return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
263-
}
264-
265156
func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) {
266157
tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name)
267158
if err != nil {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
//go:build go1.25
5+
6+
package repository
7+
8+
import (
9+
"context"
10+
"fmt"
11+
"os"
12+
"path"
13+
"path/filepath"
14+
"strings"
15+
"time"
16+
17+
repo_model "forgejo.org/models/repo"
18+
"forgejo.org/modules/git"
19+
"forgejo.org/modules/log"
20+
"forgejo.org/modules/util"
21+
)
22+
23+
func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error {
24+
commitTimeStr := time.Now().Format(time.RFC3339)
25+
authorSig := repo.Owner.NewGitSig()
26+
27+
// Because this may call hooks we should pass in the environment
28+
env := append(os.Environ(),
29+
"GIT_AUTHOR_NAME="+authorSig.Name,
30+
"GIT_AUTHOR_EMAIL="+authorSig.Email,
31+
"GIT_AUTHOR_DATE="+commitTimeStr,
32+
"GIT_COMMITTER_NAME="+authorSig.Name,
33+
"GIT_COMMITTER_EMAIL="+authorSig.Email,
34+
"GIT_COMMITTER_DATE="+commitTimeStr,
35+
)
36+
37+
// Clone to temporary path and do the init commit.
38+
templateRepoPath := templateRepo.RepoPath()
39+
if err := git.Clone(ctx, templateRepoPath, tmpDir, git.CloneRepoOptions{
40+
Depth: 1,
41+
Branch: templateRepo.DefaultBranch,
42+
}); err != nil {
43+
return fmt.Errorf("git clone: %w", err)
44+
}
45+
46+
if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil {
47+
return fmt.Errorf("remove git dir: %w", err)
48+
}
49+
50+
// Variable expansion
51+
gt, err := checkGiteaTemplate(tmpDir)
52+
if err != nil {
53+
return fmt.Errorf("checkGiteaTemplate: %w", err)
54+
}
55+
56+
if gt != nil {
57+
if err := util.Remove(gt.Path); err != nil {
58+
return fmt.Errorf("remove .giteatemplate: %w", err)
59+
}
60+
61+
// Avoid walking tree if there are no globs
62+
if len(gt.Globs()) > 0 {
63+
// All file access should be done through `root` to avoid file traversal attacks, especially with symlinks
64+
root, err := os.OpenRoot(tmpDir)
65+
if err != nil {
66+
return fmt.Errorf("open root: %w", err)
67+
}
68+
defer root.Close()
69+
70+
tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
71+
if err := filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
72+
if walkErr != nil {
73+
return walkErr
74+
}
75+
76+
if d.IsDir() {
77+
return nil
78+
}
79+
80+
base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
81+
for _, g := range gt.Globs() {
82+
if g.Match(base) {
83+
// `path` will be an absolute filepath from `WalkDir`, but `os.Root` requires all accesses are
84+
// relative file paths from the root -- use `relPath` from here out.
85+
relPath, err := filepath.Rel(tmpDir, path)
86+
if err != nil {
87+
return err
88+
}
89+
90+
content, err := root.ReadFile(relPath)
91+
if err != nil {
92+
return err
93+
}
94+
95+
if err := root.WriteFile(relPath,
96+
[]byte(generateExpansion(string(content), templateRepo, generateRepo, false)),
97+
0o644); err != nil {
98+
return err
99+
}
100+
101+
substPath := generateExpansion(relPath, templateRepo, generateRepo, true)
102+
103+
// Create parent subdirectories if needed or continue silently if it exists
104+
if err := root.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
105+
return err
106+
}
107+
108+
// Substitute filename variables
109+
if err := root.Rename(relPath, substPath); err != nil {
110+
return err
111+
}
112+
113+
break
114+
}
115+
}
116+
return nil
117+
}); err != nil {
118+
return err
119+
}
120+
}
121+
}
122+
123+
if err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
124+
return err
125+
}
126+
127+
repoPath := repo.RepoPath()
128+
if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repoPath).
129+
SetDescription(fmt.Sprintf("generateRepoCommit (git remote add): %s to %s", templateRepoPath, tmpDir)).
130+
RunStdString(&git.RunOpts{Dir: tmpDir, Env: env}); err != nil {
131+
log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err)
132+
return fmt.Errorf("git remote add: %w", err)
133+
}
134+
135+
// set default branch based on whether it's specified in the newly generated repo or not
136+
defaultBranch := repo.DefaultBranch
137+
if strings.TrimSpace(defaultBranch) == "" {
138+
defaultBranch = templateRepo.DefaultBranch
139+
}
140+
141+
return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
142+
}

0 commit comments

Comments
 (0)