fix: validate repo URL format on app creation (closes #88) #91

Merged
sneak merged 2 commits from fix/repo-url-validation into main 2026-02-20 11:58:49 +01:00
2 changed files with 12 additions and 1 deletions
Showing only changes of commit a2087f4898 - Show all commits

View File

@ -17,7 +17,8 @@ var (
) )
// scpLikeRepoRe matches SCP-like git URLs: git@host:path (e.g. git@github.com:user/repo.git). // scpLikeRepoRe matches SCP-like git URLs: git@host:path (e.g. git@github.com:user/repo.git).
var scpLikeRepoRe = regexp.MustCompile(`^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:.+$`) // Only the "git" user is allowed, as that is the standard for SSH deploy keys.
var scpLikeRepoRe = regexp.MustCompile(`^git@[a-zA-Z0-9._-]+:.+$`)

The SCP regex accepts any user (not just git) and any path after :. Consider restricting the user portion or at minimum validating the path doesn't contain path traversal (..). Example concern: admin@localhost:../../etc/shadow.

The SCP regex accepts any user (not just `git`) and any path after `:`. Consider restricting the user portion or at minimum validating the path doesn't contain path traversal (`..`). Example concern: `admin@localhost:../../etc/shadow`.
// validateRepoURL checks that the given repository URL is valid and uses an allowed scheme. // validateRepoURL checks that the given repository URL is valid and uses an allowed scheme.
func validateRepoURL(repoURL string) error { func validateRepoURL(repoURL string) error {
@ -25,6 +26,11 @@ func validateRepoURL(repoURL string) error {
return errRepoURLEmpty return errRepoURLEmpty
} }
// Reject path traversal in any URL format
if strings.Contains(repoURL, "..") {
return errRepoURLInvalid
}
// Check for SCP-like git URLs first (git@host:path) // Check for SCP-like git URLs first (git@host:path)
if scpLikeRepoRe.MatchString(repoURL) { if scpLikeRepoRe.MatchString(repoURL) {
return nil return nil

View File

@ -32,6 +32,11 @@ func TestValidateRepoURL(t *testing.T) {
{name: "no host https", url: "https:///path", wantErr: true}, {name: "no host https", url: "https:///path", wantErr: true},
{name: "no path https", url: "https://github.com", wantErr: true}, {name: "no path https", url: "https://github.com", wantErr: true},
{name: "no path https trailing slash", url: "https://github.com/", wantErr: true}, {name: "no path https trailing slash", url: "https://github.com/", wantErr: true},
{name: "SCP-like non-git user", url: "root@github.com:user/repo.git", wantErr: true},
{name: "SCP-like arbitrary user", url: "admin@github.com:user/repo.git", wantErr: true},
{name: "path traversal SCP", url: "git@github.com:../../etc/passwd", wantErr: true},
{name: "path traversal https", url: "https://github.com/user/../../../etc/passwd", wantErr: true},
{name: "path traversal in middle", url: "https://github.com/user/repo/../secret", wantErr: true},
} }
for _, tc := range tests { for _, tc := range tests {