Skip to content

Commit 449b5bf

Browse files
committed
[v13.0/forgejo] fix: prevent writing to out-of-repo symlink destinations while evaluating template repos
1 parent 8885844 commit 449b5bf

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
}
@@ -427,14 +430,14 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file
427430
}
428431

429432
// Add the object to the index
433+
mode := "100644" // regular file
430434
if file.Options.executable {
431-
if err := t.AddObjectToIndex("100755", objectHash, file.Options.treePath); err != nil {
432-
return err
433-
}
434-
} else {
435-
if err := t.AddObjectToIndex("100644", objectHash, file.Options.treePath); err != nil {
436-
return err
437-
}
435+
mode = "100755"
436+
} else if file.Options.symlink {
437+
mode = "120644"
438+
}
439+
if err := t.AddObjectToIndex(mode, objectHash, file.Options.treePath); err != nil {
440+
return err
438441
}
439442

440443
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"
@@ -150,114 +149,6 @@ func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
150149
}, nil
151150
}
152151

153-
func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error {
154-
commitTimeStr := time.Now().Format(time.RFC3339)
155-
authorSig := repo.Owner.NewGitSig()
156-
157-
// Because this may call hooks we should pass in the environment
158-
env := append(os.Environ(),
159-
"GIT_AUTHOR_NAME="+authorSig.Name,
160-
"GIT_AUTHOR_EMAIL="+authorSig.Email,
161-
"GIT_AUTHOR_DATE="+commitTimeStr,
162-
"GIT_COMMITTER_NAME="+authorSig.Name,
163-
"GIT_COMMITTER_EMAIL="+authorSig.Email,
164-
"GIT_COMMITTER_DATE="+commitTimeStr,
165-
)
166-
167-
// Clone to temporary path and do the init commit.
168-
templateRepoPath := templateRepo.RepoPath()
169-
if err := git.Clone(ctx, templateRepoPath, tmpDir, git.CloneRepoOptions{
170-
Depth: 1,
171-
Branch: templateRepo.DefaultBranch,
172-
}); err != nil {
173-
return fmt.Errorf("git clone: %w", err)
174-
}
175-
176-
if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil {
177-
return fmt.Errorf("remove git dir: %w", err)
178-
}
179-
180-
// Variable expansion
181-
gt, err := checkGiteaTemplate(tmpDir)
182-
if err != nil {
183-
return fmt.Errorf("checkGiteaTemplate: %w", err)
184-
}
185-
186-
if gt != nil {
187-
if err := util.Remove(gt.Path); err != nil {
188-
return fmt.Errorf("remove .giteatemplate: %w", err)
189-
}
190-
191-
// Avoid walking tree if there are no globs
192-
if len(gt.Globs()) > 0 {
193-
tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
194-
if err := filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
195-
if walkErr != nil {
196-
return walkErr
197-
}
198-
199-
if d.IsDir() {
200-
return nil
201-
}
202-
203-
base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
204-
for _, g := range gt.Globs() {
205-
if g.Match(base) {
206-
content, err := os.ReadFile(path)
207-
if err != nil {
208-
return err
209-
}
210-
211-
if err := os.WriteFile(path,
212-
[]byte(generateExpansion(string(content), templateRepo, generateRepo, false)),
213-
0o644); err != nil {
214-
return err
215-
}
216-
217-
substPath := filepath.FromSlash(filepath.Join(tmpDirSlash,
218-
generateExpansion(base, templateRepo, generateRepo, true)))
219-
220-
// Create parent subdirectories if needed or continue silently if it exists
221-
if err := os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
222-
return err
223-
}
224-
225-
// Substitute filename variables
226-
if err := os.Rename(path, substPath); err != nil {
227-
return err
228-
}
229-
230-
break
231-
}
232-
}
233-
return nil
234-
}); err != nil {
235-
return err
236-
}
237-
}
238-
}
239-
240-
if err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
241-
return err
242-
}
243-
244-
repoPath := repo.RepoPath()
245-
if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repoPath).
246-
SetDescription(fmt.Sprintf("generateRepoCommit (git remote add): %s to %s", templateRepoPath, tmpDir)).
247-
RunStdString(&git.RunOpts{Dir: tmpDir, Env: env}); err != nil {
248-
log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err)
249-
return fmt.Errorf("git remote add: %w", err)
250-
}
251-
252-
// set default branch based on whether it's specified in the newly generated repo or not
253-
defaultBranch := repo.DefaultBranch
254-
if strings.TrimSpace(defaultBranch) == "" {
255-
defaultBranch = templateRepo.DefaultBranch
256-
}
257-
258-
return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
259-
}
260-
261152
func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) {
262153
tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name)
263154
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)