mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-09-13 06:17:26 +00:00
fix: don't allow credentials in migrate/push mirror URL (#9064)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9064 Reviewed-by: 0ko <0ko@noreply.codeberg.org> Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
commit
3bf52efe63
12 changed files with 169 additions and 1 deletions
|
@ -121,6 +121,7 @@ type ErrInvalidCloneAddr struct {
|
||||||
IsInvalidPath bool
|
IsInvalidPath bool
|
||||||
IsProtocolInvalid bool
|
IsProtocolInvalid bool
|
||||||
IsPermissionDenied bool
|
IsPermissionDenied bool
|
||||||
|
HasCredentials bool
|
||||||
LocalPath bool
|
LocalPath bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +144,9 @@ func (err *ErrInvalidCloneAddr) Error() string {
|
||||||
if err.IsURLError {
|
if err.IsURLError {
|
||||||
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url is invalid", err.Host)
|
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url is invalid", err.Host)
|
||||||
}
|
}
|
||||||
|
if err.HasCredentials {
|
||||||
|
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url contains credentials", err.Host)
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("migration/cloning from '%s' is not allowed", err.Host)
|
return fmt.Sprintf("migration/cloning from '%s' is not allowed", err.Host)
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
"other": "wants to merge %[1]d commits from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>"
|
"other": "wants to merge %[1]d commits from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>"
|
||||||
},
|
},
|
||||||
"repo.form.cannot_create": "All spaces in which you can create repositories have reached the limit of repositories.",
|
"repo.form.cannot_create": "All spaces in which you can create repositories have reached the limit of repositories.",
|
||||||
|
"migrate.form.error.url_credentials": "The URL contains contains credentials, put them in the username and password fields respectively",
|
||||||
"repo.issue_indexer.title": "Issue Indexer",
|
"repo.issue_indexer.title": "Issue Indexer",
|
||||||
"search.milestone_kind": "Search milestones…",
|
"search.milestone_kind": "Search milestones…",
|
||||||
"repo.settings.push_mirror.branch_filter.label": "Branch filter (optional)",
|
"repo.settings.push_mirror.branch_filter.label": "Branch filter (optional)",
|
||||||
|
|
|
@ -283,6 +283,8 @@ func handleRemoteAddrError(ctx *context.APIContext, err error) {
|
||||||
}
|
}
|
||||||
case addrErr.IsInvalidPath:
|
case addrErr.IsInvalidPath:
|
||||||
ctx.Error(http.StatusUnprocessableEntity, "", "Invalid local path, it does not exist or not a directory.")
|
ctx.Error(http.StatusUnprocessableEntity, "", "Invalid local path, it does not exist or not a directory.")
|
||||||
|
case addrErr.HasCredentials:
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "", "The URL contains credentials.")
|
||||||
default:
|
default:
|
||||||
ctx.Error(http.StatusInternalServerError, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error())
|
ctx.Error(http.StatusInternalServerError, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -442,6 +442,8 @@ func HandleRemoteAddressError(ctx *context.APIContext, err error) {
|
||||||
ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid Url ")
|
ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid Url ")
|
||||||
case addrErr.IsPermissionDenied:
|
case addrErr.IsPermissionDenied:
|
||||||
ctx.Error(http.StatusUnauthorized, "CreatePushMirror", "Permission denied")
|
ctx.Error(http.StatusUnauthorized, "CreatePushMirror", "Permission denied")
|
||||||
|
case addrErr.HasCredentials:
|
||||||
|
ctx.Error(http.StatusBadRequest, "CreatePushMirror", "The URL contains credentials")
|
||||||
default:
|
default:
|
||||||
ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Unknown error")
|
ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Unknown error")
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,6 +138,8 @@ func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl base.TplN
|
||||||
}
|
}
|
||||||
case addrErr.IsInvalidPath:
|
case addrErr.IsInvalidPath:
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
|
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
|
||||||
|
case addrErr.HasCredentials:
|
||||||
|
ctx.RenderWithErr(ctx.Tr("migrate.form.error.url_credentials"), tpl, form)
|
||||||
default:
|
default:
|
||||||
log.Error("Error whilst updating url: %v", err)
|
log.Error("Error whilst updating url: %v", err)
|
||||||
ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form)
|
ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form)
|
||||||
|
|
|
@ -543,7 +543,13 @@ func SettingsPost(ctx *context.Context) {
|
||||||
|
|
||||||
mirror_service.AddPullMirrorToQueue(repo.ID)
|
mirror_service.AddPullMirrorToQueue(repo.ID)
|
||||||
|
|
||||||
ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
|
sanitizedOriginalURL, err := util.SanitizeURL(repo.OriginalURL)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("SanitizeURL", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", sanitizedOriginalURL))
|
||||||
ctx.Redirect(repo.Link() + "/settings")
|
ctx.Redirect(repo.Link() + "/settings")
|
||||||
|
|
||||||
case "push-mirror-sync":
|
case "push-mirror-sync":
|
||||||
|
@ -1110,6 +1116,8 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R
|
||||||
}
|
}
|
||||||
case addrErr.IsInvalidPath:
|
case addrErr.IsInvalidPath:
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
|
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
|
||||||
|
case addrErr.HasCredentials:
|
||||||
|
ctx.RenderWithErr(ctx.Tr("migrate.form.error.url_credentials"), tplSettingsOptions, form)
|
||||||
default:
|
default:
|
||||||
ctx.ServerError("Unknown error", err)
|
ctx.ServerError("Unknown error", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,9 @@ func ParseRemoteAddr(remoteAddr, authUsername, authPassword string) (string, err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", &models.ErrInvalidCloneAddr{IsURLError: true, Host: remoteAddr}
|
return "", &models.ErrInvalidCloneAddr{IsURLError: true, Host: remoteAddr}
|
||||||
}
|
}
|
||||||
|
if u.User != nil {
|
||||||
|
return "", &models.ErrInvalidCloneAddr{Host: remoteAddr, HasCredentials: true}
|
||||||
|
}
|
||||||
if len(authUsername)+len(authPassword) > 0 {
|
if len(authUsername)+len(authPassword) > 0 {
|
||||||
u.User = url.UserPassword(authUsername, authPassword)
|
u.User = url.UserPassword(authUsername, authPassword)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
-
|
||||||
|
id: 1001
|
||||||
|
repo_id: 1
|
||||||
|
interval: 3600
|
||||||
|
enable_prune: false
|
||||||
|
updated_unix: 0
|
||||||
|
next_update_unix: 0
|
||||||
|
lfs_enabled: false
|
||||||
|
lfs_endpoint: ""
|
|
@ -0,0 +1,34 @@
|
||||||
|
-
|
||||||
|
id: 1001
|
||||||
|
owner_id: 2
|
||||||
|
owner_name: user2
|
||||||
|
lower_name: repo1001
|
||||||
|
name: repo1001
|
||||||
|
default_branch: master
|
||||||
|
original_url: https://:TOKEN@example.com/example/example.git
|
||||||
|
num_watches: 0
|
||||||
|
num_stars: 0
|
||||||
|
num_forks: 0
|
||||||
|
num_issues: 0
|
||||||
|
num_closed_issues: 0
|
||||||
|
num_pulls: 0
|
||||||
|
num_closed_pulls: 0
|
||||||
|
num_milestones: 0
|
||||||
|
num_closed_milestones: 0
|
||||||
|
num_projects: 0
|
||||||
|
num_closed_projects: 0
|
||||||
|
is_private: false
|
||||||
|
is_empty: false
|
||||||
|
is_archived: false
|
||||||
|
is_mirror: true
|
||||||
|
status: 0
|
||||||
|
is_fork: false
|
||||||
|
fork_id: 0
|
||||||
|
is_template: false
|
||||||
|
template_id: 0
|
||||||
|
size: 0
|
||||||
|
is_fsck_enabled: true
|
||||||
|
close_issues_via_commit_in_any_branch: false
|
||||||
|
created_unix: 1751320800
|
||||||
|
updated_unix: 1751320800
|
||||||
|
topics: '[]'
|
|
@ -1,9 +1,11 @@
|
||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forgejo.org/models/db"
|
"forgejo.org/models/db"
|
||||||
|
@ -13,6 +15,7 @@ import (
|
||||||
"forgejo.org/modules/git"
|
"forgejo.org/modules/git"
|
||||||
"forgejo.org/modules/gitrepo"
|
"forgejo.org/modules/gitrepo"
|
||||||
"forgejo.org/modules/migration"
|
"forgejo.org/modules/migration"
|
||||||
|
forgejo_context "forgejo.org/services/context"
|
||||||
mirror_service "forgejo.org/services/mirror"
|
mirror_service "forgejo.org/services/mirror"
|
||||||
release_service "forgejo.org/services/release"
|
release_service "forgejo.org/services/release"
|
||||||
repo_service "forgejo.org/services/repository"
|
repo_service "forgejo.org/services/repository"
|
||||||
|
@ -101,3 +104,18 @@ func TestMirrorPull(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, initCount, count)
|
assert.Equal(t, initCount, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPullMirrorRedactCredentials(t *testing.T) {
|
||||||
|
defer unittest.OverrideFixtures("tests/integration/fixtures/TestPullMirrorRedactCredentials")()
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user2/repo1001/settings", map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, "/user2/repo1001/settings"),
|
||||||
|
"action": "mirror-sync",
|
||||||
|
}), http.StatusSeeOther)
|
||||||
|
|
||||||
|
flashCookie := session.GetCookie(forgejo_context.CookieNameFlash)
|
||||||
|
assert.NotNil(t, flashCookie)
|
||||||
|
assert.Equal(t, "info%3DPulling%2Bchanges%2Bfrom%2Bthe%2Bremote%2Bhttps%253A%252F%252Fexample.com%252Fexample%252Fexample.git%2Bat%2Bthe%2Bmoment.", flashCookie.Value)
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"forgejo.org/modules/setting"
|
"forgejo.org/modules/setting"
|
||||||
api "forgejo.org/modules/structs"
|
api "forgejo.org/modules/structs"
|
||||||
"forgejo.org/modules/test"
|
"forgejo.org/modules/test"
|
||||||
|
"forgejo.org/modules/translation"
|
||||||
gitea_context "forgejo.org/services/context"
|
gitea_context "forgejo.org/services/context"
|
||||||
doctor "forgejo.org/services/doctor"
|
doctor "forgejo.org/services/doctor"
|
||||||
"forgejo.org/services/migrations"
|
"forgejo.org/services/migrations"
|
||||||
|
@ -42,6 +43,46 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestPushMirrorRedactCredential(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
cloneAddr := "https://:TOKEN@example.com/example/example.git"
|
||||||
|
|
||||||
|
t.Run("Web route", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user2/repo1/settings", map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, "/user2/repo1/settings"),
|
||||||
|
"action": "push-mirror-add",
|
||||||
|
"push_mirror_address": cloneAddr,
|
||||||
|
"push_mirror_interval": "0",
|
||||||
|
}), http.StatusOK)
|
||||||
|
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
assert.Contains(t,
|
||||||
|
htmlDoc.doc.Find(".ui.negative.message").Text(),
|
||||||
|
translation.NewLocale("en-US").Tr("migrate.form.error.url_credentials"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("API route", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
resp := MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/push_mirrors", &api.CreatePushMirrorOption{
|
||||||
|
RemoteAddress: cloneAddr,
|
||||||
|
Interval: "0",
|
||||||
|
}).AddTokenAuth(token), http.StatusBadRequest)
|
||||||
|
|
||||||
|
var respBody map[string]any
|
||||||
|
DecodeJSON(t, resp, &respBody)
|
||||||
|
|
||||||
|
assert.Equal(t, "The URL contains credentials", respBody["message"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMirrorPush(t *testing.T) {
|
func TestMirrorPush(t *testing.T) {
|
||||||
onGiteaRun(t, testMirrorPush)
|
onGiteaRun(t, testMirrorPush)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package integration
|
package integration
|
||||||
|
@ -9,7 +10,9 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
auth_model "forgejo.org/models/auth"
|
||||||
"forgejo.org/modules/structs"
|
"forgejo.org/modules/structs"
|
||||||
|
"forgejo.org/modules/translation"
|
||||||
"forgejo.org/tests"
|
"forgejo.org/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -55,3 +58,44 @@ func TestRepoMigrate(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepoMigrateCredentials(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
cloneAddr := "https://:TOKEN@example.com/example/example.git"
|
||||||
|
|
||||||
|
t.Run("Web route", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", "/repo/migrate?service_type=1", map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, "/repo/migrate?service_type=1"),
|
||||||
|
"clone_addr": cloneAddr,
|
||||||
|
"uid": "2",
|
||||||
|
"repo_name": "example",
|
||||||
|
"service": "1",
|
||||||
|
}), http.StatusOK)
|
||||||
|
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
assert.Contains(t,
|
||||||
|
htmlDoc.doc.Find(".ui.negative.message").Text(),
|
||||||
|
translation.NewLocale("en-US").Tr("migrate.form.error.url_credentials"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("API route", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
resp := MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", &structs.MigrateRepoOptions{
|
||||||
|
CloneAddr: cloneAddr,
|
||||||
|
RepoOwnerID: 2,
|
||||||
|
RepoName: "example",
|
||||||
|
}).AddTokenAuth(token), http.StatusUnprocessableEntity)
|
||||||
|
|
||||||
|
var respBody map[string]any
|
||||||
|
DecodeJSON(t, resp, &respBody)
|
||||||
|
|
||||||
|
assert.Equal(t, "The URL contains credentials.", respBody["message"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue