Skip to content

Commit 77cab5d

Browse files
committed
fix: prevent .forgejo/template from being out-of-repo content
1 parent 7be431d commit 77cab5d

File tree

4 files changed

+117
-30
lines changed

4 files changed

+117
-30
lines changed

services/repository/generate.go

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"context"
1010
"fmt"
1111
"os"
12-
"path/filepath"
1312
"regexp"
1413
"strconv"
1514
"strings"
@@ -120,35 +119,6 @@ func (gt *GiteaTemplate) Globs() []glob.Glob {
120119
return gt.globs
121120
}
122121

123-
func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
124-
configDirs := []string{".forgejo", ".gitea"}
125-
var templateFilePath string
126-
127-
for _, dir := range configDirs {
128-
candidatePath := filepath.Join(tmpDir, dir, "template")
129-
if _, err := os.Stat(candidatePath); err == nil {
130-
templateFilePath = candidatePath
131-
break
132-
} else if !os.IsNotExist(err) {
133-
return nil, err
134-
}
135-
}
136-
137-
if templateFilePath == "" {
138-
return nil, nil
139-
}
140-
141-
content, err := os.ReadFile(templateFilePath)
142-
if err != nil {
143-
return nil, err
144-
}
145-
146-
return &GiteaTemplate{
147-
Path: templateFilePath,
148-
Content: content,
149-
}, nil
150-
}
151-
152122
func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) {
153123
tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name)
154124
if err != nil {

services/repository/generate_repo_commit.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,39 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
140140

141141
return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
142142
}
143+
144+
func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
145+
configDirs := []string{".forgejo", ".gitea"}
146+
var templateFilePath string
147+
148+
// All file access should be done through `root` to avoid file traversal attacks, especially with symlinks
149+
root, err := os.OpenRoot(tmpDir)
150+
if err != nil {
151+
return nil, fmt.Errorf("open root: %w", err)
152+
}
153+
defer root.Close()
154+
155+
for _, dir := range configDirs {
156+
candidatePath := filepath.Join(dir, "template")
157+
if _, err := root.Stat(candidatePath); err == nil {
158+
templateFilePath = candidatePath
159+
break
160+
} else if !os.IsNotExist(err) {
161+
return nil, err
162+
}
163+
}
164+
165+
if templateFilePath == "" {
166+
return nil, nil
167+
}
168+
169+
content, err := root.ReadFile(templateFilePath)
170+
if err != nil {
171+
return nil, err
172+
}
173+
174+
return &GiteaTemplate{
175+
Path: templateFilePath,
176+
Content: content,
177+
}, nil
178+
}

services/repository/generate_repo_commit_legacy.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,44 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
163163

164164
return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
165165
}
166+
167+
func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
168+
configDirs := []string{".forgejo", ".gitea"}
169+
var templateFilePath string
170+
171+
// All file access should be done through `root` to avoid file traversal attacks, especially with symlinks
172+
root, err := os.OpenRoot(tmpDir)
173+
if err != nil {
174+
return nil, fmt.Errorf("open root: %w", err)
175+
}
176+
defer root.Close()
177+
178+
for _, dir := range configDirs {
179+
candidatePath := filepath.Join(dir, "template")
180+
if _, err := root.Stat(candidatePath); err == nil {
181+
templateFilePath = candidatePath
182+
break
183+
} else if !os.IsNotExist(err) {
184+
return nil, err
185+
}
186+
}
187+
188+
if templateFilePath == "" {
189+
return nil, nil
190+
}
191+
192+
// FIXME: root.ReadFile(relPath) in go 1.25
193+
file, err := root.Open(templateFilePath)
194+
if err != nil {
195+
return nil, err
196+
}
197+
content, err := io.ReadAll(file)
198+
if err != nil {
199+
return nil, err
200+
}
201+
202+
return &GiteaTemplate{
203+
Path: templateFilePath,
204+
Content: content,
205+
}, nil
206+
}

tests/integration/repo_generate_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,43 @@ func TestRepoGenerateTemplatingSymlink(t *testing.T) {
406406
}
407407
})
408408
}
409+
410+
func TestRepoGenerateTemplatingSymlinkGlobFile(t *testing.T) {
411+
onApplicationRun(t, func(t *testing.T, u *url.URL) {
412+
templateName := "my_template"
413+
generatedName := "my_generated"
414+
415+
userName := "user1"
416+
session := loginUser(t, userName)
417+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: userName})
418+
419+
template, _, f := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{
420+
Name: optional.Some(templateName),
421+
IsTemplate: optional.Some(true),
422+
Files: optional.Some([]*files_service.ChangeRepoFile{
423+
{
424+
Operation: "create",
425+
TreePath: ".forgejo/template",
426+
ContentReader: strings.NewReader("/etc/passwd"),
427+
Symlink: true,
428+
},
429+
}),
430+
})
431+
defer f()
432+
433+
// The repo.TemplateID field is not initialized. Luckily, the ID field holds the expected value
434+
templateID := strconv.FormatInt(template.ID, 10)
435+
436+
resp := testRepoGenerateFailure(
437+
t,
438+
session,
439+
templateID,
440+
user.Name,
441+
templateName,
442+
user,
443+
user,
444+
generatedName,
445+
)
446+
assert.Contains(t, resp.Body.String(), "statat .forgejo/template: path escapes from parent")
447+
})
448+
}

0 commit comments

Comments
 (0)