Securing Gitea Repository Templates: A Deep Dive into PR #36734 & #36746
- Part 1: How fzf Just Became Way More Memory-Efficient: The Bitmap Cache Upgrade
- Part 2: Unlocking Faster Fuzzy Finding: How a Smart Work Queue Made fzf Even Quicker
- Part 3: Speeding Up fzf: How a Tiny Change Made the World's Fastest Fuzzy Finder Even Faster
- Part 4: Fixing a Sneaky XSS Vulnerability in Hugo: Inside the Commit That Makes Markdown Rendering Safer
- Part 5: Fixing Syncthing’s REST API: Why “application/json; charset=utf-8” Was Wrong and How a Simple Commit Made It Right
- Part 6: Fixing UI Lag | Lazygit just got a major speed boost for one of its most frustrating pain points
- Part 7: Traefik PR #12880: A Tiny Fix That Squashes Unnecessary Allocations and Kills Routing Lag
- Part 8: Fixing Header Sanitization in Traefik’s PassTLSClientCert Middleware: A Deep Dive into PR #12875
- Part 9: Fixing URL Prefix Stripping in Traefik: Inside Pull Request #12863
- Part 10: Gitea Just Got More Secure: Fixing OAuth2 Authorization Code Expiry and Reuse
- Part 11: Fixing a Sneaky Security Bug in Gitea: Users Could No Longer Change Someone Else’s Primary Email
- Part 12: Securing Gitea Repository Templates: A Deep Dive into PR #36734 & #36746
- Part 13: Gitea's Container Registry Just Got More Reliable: Fixing Data Races in Concurrent Blob Uploads (PR #36524 + Backport #36526)
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
.gitcleanup. - 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:
- Clones the template into a temporary folder (
tmpDir). - Looks for special files under
.gitea/template. - Reads those files, expands variables (repo name, owner, etc.), and writes the result back.
- 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/configleaking 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):
- Prevent symlink attacks during template processing.
- Never leave
.gitmetadata behind in generated repositories. - Keep the existing template-expansion behavior 100% intact for honest users.
- 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):
- Split the relative path into components (
a/b/c). - Walk each component one-by-one from the
root. - Use
os.Lstat(notStat— important!) to check the real file type without following symlinks. - Every intermediate component must be a regular directory.
- The final component must be a regular file (for Read) or we create one (for Write).
- 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/templateis now explicitly removed after processing. - The entire
.gitdirectory 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 đź”—
- Author & main implementer: @wxiaoguang — thank you!
- Reviewers: @lunny & @Zettat123
- Backport: GiteaBot
- Merged into: 1.26.0 (main) and 1.25.x
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
.gitleftovers 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 .
- Part 1: How fzf Just Became Way More Memory-Efficient: The Bitmap Cache Upgrade
- Part 2: Unlocking Faster Fuzzy Finding: How a Smart Work Queue Made fzf Even Quicker
- Part 3: Speeding Up fzf: How a Tiny Change Made the World's Fastest Fuzzy Finder Even Faster
- Part 4: Fixing a Sneaky XSS Vulnerability in Hugo: Inside the Commit That Makes Markdown Rendering Safer
- Part 5: Fixing Syncthing’s REST API: Why “application/json; charset=utf-8” Was Wrong and How a Simple Commit Made It Right
- Part 6: Fixing UI Lag | Lazygit just got a major speed boost for one of its most frustrating pain points
- Part 7: Traefik PR #12880: A Tiny Fix That Squashes Unnecessary Allocations and Kills Routing Lag
- Part 8: Fixing Header Sanitization in Traefik’s PassTLSClientCert Middleware: A Deep Dive into PR #12875
- Part 9: Fixing URL Prefix Stripping in Traefik: Inside Pull Request #12863
- Part 10: Gitea Just Got More Secure: Fixing OAuth2 Authorization Code Expiry and Reuse
- Part 11: Fixing a Sneaky Security Bug in Gitea: Users Could No Longer Change Someone Else’s Primary Email
- Part 12: Securing Gitea Repository Templates: A Deep Dive into PR #36734 & #36746
- Part 13: Gitea's Container Registry Just Got More Reliable: Fixing Data Races in Concurrent Blob Uploads (PR #36524 + Backport #36526)