Skip to content

Commit bd89c7f

Browse files
committed
[v11.0/forgejo] fix: prevent .forgejo/template from being out-of-repo content
1 parent 328a0b4 commit bd89c7f

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"
@@ -124,35 +123,6 @@ func (gt *GiteaTemplate) Globs() []glob.Glob {
124123
return gt.globs
125124
}
126125

127-
func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
128-
configDirs := []string{".forgejo", ".gitea"}
129-
var templateFilePath string
130-
131-
for _, dir := range configDirs {
132-
candidatePath := filepath.Join(tmpDir, dir, "template")
133-
if _, err := os.Stat(candidatePath); err == nil {
134-
templateFilePath = candidatePath
135-
break
136-
} else if !os.IsNotExist(err) {
137-
return nil, err
138-
}
139-
}
140-
141-
if templateFilePath == "" {
142-
return nil, nil
143-
}
144-
145-
content, err := os.ReadFile(templateFilePath)
146-
if err != nil {
147-
return nil, err
148-
}
149-
150-
return &GiteaTemplate{
151-
Path: templateFilePath,
152-
Content: content,
153-
}, nil
154-
}
155-
156126
func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) {
157127
tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name)
158128
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
@@ -344,3 +344,43 @@ func TestRepoGenerateTemplatingSymlink(t *testing.T) {
344344
}
345345
})
346346
}
347+
348+
func TestRepoGenerateTemplatingSymlinkGlobFile(t *testing.T) {
349+
onGiteaRun(t, func(t *testing.T, u *url.URL) {
350+
templateName := "my_template"
351+
generatedName := "my_generated"
352+
353+
userName := "user1"
354+
session := loginUser(t, userName)
355+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: userName})
356+
357+
template, _, f := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{
358+
Name: optional.Some(templateName),
359+
IsTemplate: optional.Some(true),
360+
Files: optional.Some([]*files_service.ChangeRepoFile{
361+
{
362+
Operation: "create",
363+
TreePath: ".forgejo/template",
364+
ContentReader: strings.NewReader("/etc/passwd"),
365+
Symlink: true,
366+
},
367+
}),
368+
})
369+
defer f()
370+
371+
// The repo.TemplateID field is not initialized. Luckily, the ID field holds the expected value
372+
templateID := strconv.FormatInt(template.ID, 10)
373+
374+
resp := testRepoGenerateFailure(
375+
t,
376+
session,
377+
templateID,
378+
user.Name,
379+
templateName,
380+
user,
381+
user,
382+
generatedName,
383+
)
384+
assert.Contains(t, resp.Body.String(), "statat .forgejo/template: path escapes from parent")
385+
})
386+
}

0 commit comments

Comments
 (0)