All Posts programming Securing Gitea Repository Templates: A Deep Dive into PR #36734 & #36746

Securing Gitea Repository Templates: A Deep Dive into PR #36734 & #36746

· 818 words · 4 minute read
Go in production: interesting moments ▹

Fixing path traversal vulnerabilities with safe file handling đź”—

TL;DR (for the hurried reader)

  • Problem: Gitea’s repository template system (.gitea/template) could be tricked by symlinks into reading/writing files outside the intended temporary directory — a classic path-traversal security risk.
  • Fix: Two new helper functions enforce “regular file only” paths (no symlinks!) + explicit .git cleanup.
  • Result: Safer template generation in Gitea 1.26+ (and backported to 1.25).
  • PRs: #36734 (main) by wxiaoguang + #36746 (backport) by GiteaBot.
  • Status: Both merged February 25, 2026.

Scroll for the full story — or jump straight to the code if you’re in a hurry.

1. The Problem: “Why did this even need fixing?” 🔗

When you create a new repository from a template in Gitea, the system:

  1. Clones the template into a temporary folder (tmpDir).
  2. Looks for special files under .gitea/template.
  3. Reads those files, expands variables (repo name, owner, etc.), and writes the result back.
  4. Turns the temporary folder into the new repo’s initial commit.

The old code used plain os.ReadFile, os.WriteFile, and filepath.Join.

That worked fine for normal files… but symlinks could break the rules:

  • A malicious template could include a symlink that points outside tmpDir.
  • The code would happily follow it and read/write files in the real filesystem.
  • Result: potential data leak, config overwrite, or even .git/config leaking into the generated repo.

This is exactly the kind of issue that gets labeled topic/security.

(There was an earlier attempt in closed PR #36731 “Fix template path possible symlink”, but #36734 nailed the complete solution.)

2. The Why: Security + Clean Output đź”—

Goals of the PRs (clear from the start):

  1. Prevent symlink attacks during template processing.
  2. Never leave .git metadata behind in generated repositories.
  3. Keep the existing template-expansion behavior 100% intact for honest users.
  4. Add tests so the fix can never regress.

All four goals are fully achieved by the end of this post.

3. The How: New Safe Path Utilities (the heart of the fix) đź”—

wxiaoguang added three tiny but powerful helpers in modules/util/path.go:

// FilePathJoinAbs – always returns a clean absolute path
func FilePathJoinAbs(elems ...string) string { ... }

// ReadRegularPathFile – ONLY reads regular files, rejects symlinks
func ReadRegularPathFile(root, filePathIn string, limit int) ([]byte, error)

// WriteRegularPathFile – ONLY writes regular files, creates safe dirs
func WriteRegularPathFile(root, filePathIn string, data []byte, dirMode, fileMode os.FileMode) error

How they work (technical explanation, step-by-step):

  1. Split the relative path into components (a/b/c).
  2. Walk each component one-by-one from the root.
  3. Use os.Lstat (not Stat — important!) to check the real file type without following symlinks.
  4. Every intermediate component must be a regular directory.
  5. The final component must be a regular file (for Read) or we create one (for Write).
  6. If anything is a symlink → return ErrNotRegularPathFile.
var ErrNotRegularPathFile = errors.New("not a regular file")

This is the key security change. No more blind trust in the filesystem.

4. Where the helpers were used: services/repository/generate.go đź”—

The template-processing functions were updated in two places:

Before (unsafe):

content, err := os.ReadFile(fullPath)
...
os.WriteFile(...)

After (safe):

content, err := util.ReadRegularPathFile(tmpDir, tmpDirSubPath, 1024*1024)
...
return util.WriteRegularPathFile(tmpDir, substSubPath, []byte(generatedContent), 0o755, 0o644)

Plus two cleanups:

  • The marker file .gitea/template is now explicitly removed after processing.
  • The entire .git directory that the template clone may have created is deleted:
if err = util.RemoveAll(util.FilePathJoinAbs(tmpDir, ".git")); err != nil {
    return nil, err
}

No more accidental .git/config ending up in your shiny new repo!

5. Proof It Works: Tests Added đź”—

New tests in modules/util/path_test.go cover:

  • âś… Reading a normal file → succeeds
  • âś… Reading through a symlink → ErrNotRegularPathFile
  • âś… Writing into a nested path → succeeds
  • âś… Writing to an existing symlink → rejected
  • âś… Edge cases with .. and absolute paths

The existing generate_test.go suite now implicitly tests the safe path behavior too.

6. The Backport: PR #36746 đź”—

Because this was a security fix, it was immediately backported to the stable release/v1.25 branch by GiteaBot (PR #36746).

Same code, same safety, now available to everyone on 1.25.x too.

7. Who Made It Happen đź”—

8. Conclusion: Goals Achieved âś… đź”—

By the end of this change:

  • Path traversal via symlinks is now impossible during template generation.
  • Only regular files are ever read or written.
  • No .git leftovers pollute generated repositories.
  • Behavior for normal templates is unchanged.
  • Tests guarantee the fix stays solid forever.

Gitea’s repository template feature is now safer, cleaner, and more predictable.

If you maintain a Gitea instance, update to 1.26+ (or 1.25 with the backport) and you’re good to go.

I hope you enjoyed reading this post as much as I enjoyed writing it. If you know a person who can benefit from this information, send them a link of this post. If you want to get notified about new posts, follow me on YouTube , Twitter (x) , LinkedIn , and GitHub .

Go in production: interesting moments ▹