Skip to content

Commit 1c696b2

Browse files
mfenniakMathieu Fenniak
authored andcommitted
fix: prevent mirroring & migrations on unencrypted URL schemes (http://, git://) (#13490)
Requires that all outbound network connections made by migrations & mirroring be performed over encrypted communication channels. As those communication channels (HTTPS and SSH) have their own built-in protections against DNS rebinding (TLS certificate validation and SSH host key validation), DNS rebinding attacks become ineffective as a means to redirect Forgejo to pull-from or push-to unexpected remotes with mirrors. `[migrations].ALLOW_UNENCRYPTED` is a new setting which defaults to false, and can be set to `true` to restore access to unencrypted remotes. The risks associated with it are documented in `app.example.ini` and will be transferred into the docs repo. **Breaking:** Existing mirrors configured against `http://` and `git://` protocols will fail, with "unencrypted transfer protocol, remote cannot be validated". It is recommended that those mirrors be changed to encrypted protocols. `[migrations].ALLOW_UNENCRYPTED` may be set to `true` to revert to the previous behaviour, accepting the risks documented here. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/13490 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org> Reviewed-by: Gusted <gusted@noreply.codeberg.org>
1 parent 4e1c50f commit 1c696b2

11 files changed

Lines changed: 77 additions & 0 deletions

File tree

custom/conf/app.example.ini

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2559,6 +2559,13 @@ LEVEL = Info
25592559
;;
25602560
;; If set to true, completely ignores server certificate validation errors. This option is unsafe.
25612561
;SKIP_TLS_VERIFY = false
2562+
;;
2563+
;; Allow insecure connections for migrations, including http:// and git:// protocols. Credentials
2564+
;; and data will be in plain-text. ALLOWED_DOMAINS, BLOCKED_DOMAINS, and ALLOW_LOCALNETWORKS
2565+
;; protections may be bypassed by DNS rebinding attacks, allowing access to unprotected resources
2566+
;; on your network. This may be appropriate for short-term usage during a Forgejo migration,
2567+
;; but should be disabled in long-term production environments.
2568+
;ALLOW_UNENCRYPTED = false
25622569

25632570
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
25642571
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

models/error.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ type ErrInvalidCloneAddr struct {
123123
IsPermissionDenied bool
124124
HasCredentials bool
125125
LocalPath bool
126+
Unencrypted bool
126127
}
127128

128129
// IsErrInvalidCloneAddr checks if an error is a ErrInvalidCloneAddr.
@@ -147,6 +148,9 @@ func (err *ErrInvalidCloneAddr) Error() string {
147148
if err.HasCredentials {
148149
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url contains credentials", err.Host)
149150
}
151+
if err.Unencrypted {
152+
return fmt.Sprintf("migration/cloning from '%s' is not allowed: unencrypted transfer protocols are disabled because the authenticity of the remote cannot be validated", err.Host)
153+
}
150154

151155
return fmt.Sprintf("migration/cloning from '%s' is not allowed", err.Host)
152156
}

modules/setting/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var Migrations = struct {
1111
BlockedDomains string
1212
AllowLocalNetworks bool
1313
SkipTLSVerify bool
14+
AllowUnencrypted bool
1415
}{
1516
MaxAttempts: 3,
1617
RetryBackoff: 3,
@@ -25,4 +26,5 @@ func loadMigrationsFrom(rootCfg ConfigProvider) {
2526
Migrations.BlockedDomains = sec.Key("BLOCKED_DOMAINS").MustString("")
2627
Migrations.AllowLocalNetworks = sec.Key("ALLOW_LOCALNETWORKS").MustBool(false)
2728
Migrations.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool(false)
29+
Migrations.AllowUnencrypted = sec.Key("ALLOW_UNENCRYPTED").MustBool(false)
2830
}

services/migrations/allowlist/is_migrate_allowed.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ func isURLAllowed(remoteURL string, doer *user_model.User, isPushMirror bool) er
6868
return &models.ErrInvalidCloneAddr{Host: u.Host, IsProtocolInvalid: true, IsPermissionDenied: true, IsURLError: true}
6969
}
7070

71+
if (u.Scheme == "http" || u.Scheme == "git") && !setting.Migrations.AllowUnencrypted {
72+
return &models.ErrInvalidCloneAddr{Host: remoteURL, Unencrypted: true}
73+
}
74+
7175
hostName, _, err := net.SplitHostPort(u.Host)
7276
if err != nil {
7377
// u.Host can be "host" or "host:port"

services/migrations/allowlist/is_migrate_allowed_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"forgejo.org/models/unittest"
1313
user_model "forgejo.org/models/user"
1414
"forgejo.org/modules/setting"
15+
"forgejo.org/modules/test"
1516

1617
"github.com/stretchr/testify/require"
1718
)
@@ -74,6 +75,38 @@ func TestMigrateWhiteBlocklist(t *testing.T) {
7475
require.NoError(t, err)
7576

7677
setting.ImportLocalPaths = old
78+
79+
t.Run("require encrypted", func(t *testing.T) {
80+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, false)()
81+
82+
err = IsMigrateURLAllowed("http://10.0.0.1/go-gitea/gitea.git", nonAdminUser)
83+
require.ErrorContains(t, err, "unencrypted transfer protocol")
84+
85+
err = IsMigrateURLAllowed("git://10.0.0.1/go-gitea/gitea.git", nonAdminUser)
86+
require.ErrorContains(t, err, "unencrypted transfer protocol")
87+
88+
err = IsMigrateURLAllowed("https://10.0.0.1/go-gitea/gitea.git", nonAdminUser)
89+
require.NoError(t, err)
90+
91+
err = IsPushMirrorURLAllowed("ssh://user@10.0.0.1/go-gitea/gitea.git", nonAdminUser)
92+
require.NoError(t, err)
93+
})
94+
95+
t.Run("permit unencrypted", func(t *testing.T) {
96+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
97+
98+
err = IsMigrateURLAllowed("http://10.0.0.1/go-gitea/gitea.git", nonAdminUser)
99+
require.NoError(t, err)
100+
101+
err = IsMigrateURLAllowed("git://10.0.0.1/go-gitea/gitea.git", nonAdminUser)
102+
require.NoError(t, err)
103+
104+
err = IsMigrateURLAllowed("https://10.0.0.1/go-gitea/gitea.git", nonAdminUser)
105+
require.NoError(t, err)
106+
107+
err = IsPushMirrorURLAllowed("ssh://user@10.0.0.1/go-gitea/gitea.git", nonAdminUser)
108+
require.NoError(t, err)
109+
})
77110
}
78111

79112
func TestAllowBlockList(t *testing.T) {

tests/integration/api_push_mirror_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func TestAPIPushMirror(t *testing.T) {
4545

4646
func testAPIPushMirror(t *testing.T, u *url.URL) {
4747
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
48+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
4849
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
4950
defer test.MockProtect(&mirror_service.AddPushMirrorRemote)()
5051
defer test.MockProtect(&repo_model.DeletePushMirrors)()
@@ -149,6 +150,7 @@ func TestAPIPushMirrorBranchFilter(t *testing.T) {
149150

150151
func testAPIPushMirrorBranchFilter(t *testing.T, u *url.URL) {
151152
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
153+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
152154
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
153155
defer test.MockProtect(&mirror_service.AddPushMirrorRemote)()
154156
defer test.MockProtect(&repo_model.DeletePushMirrors)()

tests/integration/dump_restore_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
base "forgejo.org/modules/migration"
2121
"forgejo.org/modules/setting"
2222
"forgejo.org/modules/structs"
23+
"forgejo.org/modules/test"
2324
"forgejo.org/services/migrations"
2425
migrations_allowlist "forgejo.org/services/migrations/allowlist"
2526

@@ -30,6 +31,8 @@ import (
3031

3132
func TestDumpRestore(t *testing.T) {
3233
onApplicationRun(t, func(t *testing.T, u *url.URL) {
34+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
35+
3336
AllowLocalNetworks := setting.Migrations.AllowLocalNetworks
3437
setting.Migrations.AllowLocalNetworks = true
3538
AppVer := setting.AppVer

tests/integration/migrate_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func TestMigrateLocalPath(t *testing.T) {
6262
func TestMigrate(t *testing.T) {
6363
onApplicationRun(t, func(t *testing.T, u *url.URL) {
6464
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
65+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
6566
defer test.MockVariableValue(&setting.AppVer, "1.16.0")()
6667
require.NoError(t, migrations_allowlist.Init())
6768

@@ -117,6 +118,7 @@ func TestMigrate(t *testing.T) {
117118
func TestMigrateWithIssueComments(t *testing.T) {
118119
onApplicationRun(t, func(t *testing.T, u *url.URL) {
119120
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
121+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
120122
defer test.MockVariableValue(&setting.AppVer, "1.16.0")()
121123
maxResponseItems := 10
122124
numComments := 21
@@ -193,6 +195,7 @@ func TestMigrateWithIssueComments(t *testing.T) {
193195
func TestMigrateWithWiki(t *testing.T) {
194196
onApplicationRun(t, func(t *testing.T, u *url.URL) {
195197
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
198+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
196199
defer test.MockVariableValue(&setting.AppVer, "1.16.0")()
197200
require.NoError(t, migrations_allowlist.Init())
198201

@@ -249,6 +252,7 @@ func TestMigrateWithWiki(t *testing.T) {
249252
func TestMigrateWithReleases(t *testing.T) {
250253
onApplicationRun(t, func(t *testing.T, u *url.URL) {
251254
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
255+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
252256
defer test.MockVariableValue(&setting.AppVer, "1.16.0")()
253257
require.NoError(t, migrations_allowlist.Init())
254258

tests/integration/mirror_pull_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import (
4646
)
4747

4848
func TestMirrorPull(t *testing.T) {
49+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
50+
4951
t.Run("Basic", func(t *testing.T) {
5052
defer tests.PrepareTestEnv(t)()
5153

@@ -342,6 +344,8 @@ func TestMirrorPull(t *testing.T) {
342344
// Verifies that a pull mirror which was created while the remote address was permitted will fail to sync if the
343345
// AllowedDomains configuration later changes such that the remote URL is no longer permitted.
344346
func TestMirrorPullAddressCheck(t *testing.T) {
347+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
348+
345349
// Allow localhost as a migration domain so the mirror can initially be created from the local test server. Not
346350
// using MockVariableValue due to need to undo `migrations_allowlist.Init()`.
347351
prev := setting.Migrations.AllowedDomains
@@ -651,6 +655,8 @@ func TestPullMirrorRedactCredentials(t *testing.T) {
651655
}
652656

653657
func TestMirrorPullLFS(t *testing.T) {
658+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
659+
654660
// Not using MockVariableValue due to need to undo `migrations_allowlist.Init()`
655661
prev := setting.Migrations.AllowedDomains
656662
setting.Migrations.AllowedDomains = "localhost"

tests/integration/mirror_push_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func TestMirrorPush(t *testing.T) {
8989

9090
func testMirrorPush(t *testing.T, u *url.URL) {
9191
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
92+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
9293

9394
require.NoError(t, migrations_allowlist.Init())
9495

@@ -388,6 +389,7 @@ func TestSSHPushMirror(t *testing.T) {
388389
func TestPushMirrorBranchFilterWebUI(t *testing.T) {
389390
onApplicationRun(t, func(t *testing.T, u *url.URL) {
390391
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
392+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
391393
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
392394
require.NoError(t, migrations_allowlist.Init())
393395

@@ -488,6 +490,7 @@ func TestPushMirrorBranchFilterWebUI(t *testing.T) {
488490
func TestPushMirrorBranchFilterIntegration(t *testing.T) {
489491
onApplicationRun(t, func(t *testing.T, u *url.URL) {
490492
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
493+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
491494
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
492495
require.NoError(t, migrations_allowlist.Init())
493496

@@ -577,6 +580,7 @@ func TestPushMirrorBranchFilterIntegration(t *testing.T) {
577580
func TestPushMirrorSettings(t *testing.T) {
578581
onApplicationRun(t, func(t *testing.T, u *url.URL) {
579582
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
583+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
580584
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
581585
require.NoError(t, migrations_allowlist.Init())
582586

@@ -648,6 +652,7 @@ func TestPushMirrorSettings(t *testing.T) {
648652
func TestPushMirrorBranchFilterSyncOperations(t *testing.T) {
649653
onApplicationRun(t, func(t *testing.T, u *url.URL) {
650654
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
655+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
651656
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
652657
require.NoError(t, migrations_allowlist.Init())
653658

@@ -882,6 +887,7 @@ func TestPushMirrorBranchFilterSyncOperations(t *testing.T) {
882887
func TestPushMirrorWebUIToAPIIntegration(t *testing.T) {
883888
onApplicationRun(t, func(t *testing.T, u *url.URL) {
884889
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
890+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
885891
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
886892
require.NoError(t, migrations_allowlist.Init())
887893

@@ -1097,6 +1103,8 @@ func TestPushMirrorWebUIToAPIIntegration(t *testing.T) {
10971103
}
10981104

10991105
func TestMirrorPushFailOnRedirect(t *testing.T) {
1106+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
1107+
11001108
// Not using MockVariableValue due to need to undo `migrations_allowlist.Init()`
11011109
prev := setting.Migrations.AllowedDomains
11021110
setting.Migrations.AllowedDomains = "127.0.0.1"
@@ -1152,6 +1160,8 @@ func TestMirrorPushFailOnRedirect(t *testing.T) {
11521160
// Verifies that a push mirror which was created while the remote address was permitted will fail to sync if the
11531161
// AllowedDomains configuration later changes such that the remote URL is no longer permitted.
11541162
func TestMirrorPushAddressCheck(t *testing.T) {
1163+
defer test.MockVariableValue(&setting.Migrations.AllowUnencrypted, true)()
1164+
11551165
// Not using MockVariableValue due to need to undo `migrations_allowlist.Init()`
11561166
prev := setting.Migrations.AllowedDomains
11571167
setting.Migrations.AllowedDomains = "127.0.0.1"

0 commit comments

Comments
 (0)