auth: trust reverse proxy auth header only from configured proxies (#8264)

This commit is contained in:
ᴊᴏᴇ ᴄʜᴇɴ
2026-05-18 13:42:46 -04:00
committed by GitHub
parent 6734dd46c3
commit 0089c4c8e5
9 changed files with 111 additions and 2 deletions
+1
View File
@@ -7,6 +7,7 @@ All notable changes to Gogs are documented in this file.
### Fixed
- _Security:_ Denial of service in repository and wiki file listing pages via crafted file names. [#8116](https://github.com/gogs/gogs/pull/8116) - [GHSA-3qq3-668m-v9mj](https://github.com/gogs/gogs/security/advisories/GHSA-3qq3-668m-v9mj)
- _Security:_ Reverse proxy authentication header was honored from any remote address, allowing user impersonation when Gogs was reachable directly. The header is now only trusted from addresses listed in `[auth] TRUSTED_PROXY_IPS`. [#8264](https://github.com/gogs/gogs/pull/8264) - [GHSA-w6j9-vw59-27wv](https://github.com/gogs/gogs/security/advisories/GHSA-w6j9-vw59-27wv)
### Removed
+3
View File
@@ -233,6 +233,9 @@ ENABLE_REVERSE_PROXY_AUTHENTICATION = false
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false
; The HTTP header used as username for reverse proxy authentication.
REVERSE_PROXY_AUTHENTICATION_HEADER = X-WEBAUTH-USER
; Lists the IPs or CIDR ranges whose requests are allowed to set the reverse
; proxy authentication header.
TRUSTED_PROXY_IPS = 127.0.0.0/8,::1/128
[user]
; Whether to enable email notifications for users.
+1
View File
@@ -1284,6 +1284,7 @@ config.auth.enable_registration_captcha = Enable registration captcha
config.auth.enable_reverse_proxy_authentication = Enable reverse proxy authentication
config.auth.enable_reverse_proxy_auto_registration = Enable reverse proxy auto registration
config.auth.reverse_proxy_authentication_header = Reverse proxy authentication header
config.auth.trusted_proxy_ips = Trusted proxy IPs
config.user_config = User configuration
config.user.enable_email_notify = Enable email notification
+21
View File
@@ -1,6 +1,7 @@
package conf
import (
"net"
"net/mail"
"net/url"
"os"
@@ -221,6 +222,26 @@ func Init(customConf string) error {
if err = File.Section("auth").MapTo(&Auth); err != nil {
return errors.Wrap(err, "mapping [auth] section")
}
// Reset before re-parsing so repeated Init calls (e.g. via the web installer)
// do not carry over CIDRs from a previous configuration.
Auth.TrustedProxyCIDRs = nil
for _, raw := range Auth.TrustedProxyIPs {
// Allow bare IPs as a convenience by promoting them to single-host CIDRs.
if !strings.Contains(raw, "/") {
if ip := net.ParseIP(raw); ip != nil {
if ip.To4() != nil {
raw += "/32"
} else {
raw += "/128"
}
}
}
_, cidr, err := net.ParseCIDR(raw)
if err != nil {
return errors.Wrapf(err, "parse trusted proxy CIDR %q", raw)
}
Auth.TrustedProxyCIDRs = append(Auth.TrustedProxyCIDRs, cidr)
}
// *************************
// ----- User settings -----
+6 -1
View File
@@ -2,6 +2,7 @@ package conf
import (
"fmt"
"net"
"net/url"
"os"
"time"
@@ -252,7 +253,11 @@ type AuthOpts struct {
EnableReverseProxyAuthentication bool
EnableReverseProxyAutoRegistration bool
ReverseProxyAuthenticationHeader string
CustomLogoutURL string `ini:"CUSTOM_LOGOUT_URL"`
TrustedProxyIPs []string `ini:"TRUSTED_PROXY_IPS"`
CustomLogoutURL string `ini:"CUSTOM_LOGOUT_URL"`
// Derived from other static values
TrustedProxyCIDRs []*net.IPNet `ini:"-"` // Parsed CIDR form of TrustedProxyIPs.
}
// Authentication settings
+1
View File
@@ -106,6 +106,7 @@ ENABLE_REGISTRATION_CAPTCHA=true
ENABLE_REVERSE_PROXY_AUTHENTICATION=false
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION=false
REVERSE_PROXY_AUTHENTICATION_HEADER=X-FORWARDED-FOR
TRUSTED_PROXY_IPS=127.0.0.0/8,::1/128
CUSTOM_LOGOUT_URL=
[user]
+28 -1
View File
@@ -2,6 +2,7 @@ package context
import (
"context"
"net"
"net/http"
"net/url"
"strings"
@@ -198,7 +199,7 @@ func authenticatedUser(store AuthStore, ctx *macaron.Context, sess session.Store
uid, isTokenAuth := authenticatedUserID(store, ctx, sess)
if uid <= 0 {
if conf.Auth.EnableReverseProxyAuthentication {
if conf.Auth.EnableReverseProxyAuthentication && isRequestFromTrustedProxy(ctx.Req.Request) {
webAuthUser := ctx.Req.Header.Get(conf.Auth.ReverseProxyAuthenticationHeader)
if len(webAuthUser) > 0 {
user, err := store.GetUserByUsername(ctx.Req.Context(), webAuthUser)
@@ -257,6 +258,32 @@ func authenticatedUser(store AuthStore, ctx *macaron.Context, sess session.Store
return u, false, isTokenAuth
}
// isRequestFromTrustedProxy reports whether the request's immediate remote
// address falls within one of the configured trusted proxy CIDR ranges. The
// reverse proxy authentication header is only honored for such requests so an
// attacker reaching Gogs directly cannot forge it.
func isRequestFromTrustedProxy(req *http.Request) bool {
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return false
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
// Normalize IPv4-mapped IPv6 (e.g. "::ffff:127.0.0.1" on dual-stack listeners)
// to its IPv4 form so it matches IPv4 CIDRs like 127.0.0.0/8.
if v4 := ip.To4(); v4 != nil {
ip = v4
}
for _, cidr := range conf.Auth.TrustedProxyCIDRs {
if cidr.Contains(ip) {
return true
}
}
return false
}
// AuthenticateByToken attempts to authenticate a user by the given access
// token. It returns database.ErrAccessTokenNotExist when the access token does not
// exist.
+48
View File
@@ -0,0 +1,48 @@
package context
import (
"net"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"gogs.io/gogs/internal/conf"
)
func TestIsRequestFromTrustedProxy(t *testing.T) {
mustCIDR := func(s string) *net.IPNet {
_, n, err := net.ParseCIDR(s)
require.NoError(t, err)
return n
}
original := conf.Auth.TrustedProxyCIDRs
t.Cleanup(func() { conf.Auth.TrustedProxyCIDRs = original })
conf.Auth.TrustedProxyCIDRs = []*net.IPNet{
mustCIDR("127.0.0.0/8"),
mustCIDR("::1/128"),
mustCIDR("10.1.0.0/16"),
}
tests := []struct {
name string
remoteAddr string
want bool
}{
{name: "loopback IPv4 with port", remoteAddr: "127.0.0.1:54321", want: true},
{name: "loopback IPv6 with port", remoteAddr: "[::1]:54321", want: true},
{name: "within configured CIDR", remoteAddr: "10.1.2.3:8080", want: true},
{name: "outside configured CIDR", remoteAddr: "203.0.113.5:443", want: false},
{name: "IPv4-mapped IPv6 matches IPv4 CIDR", remoteAddr: "[::ffff:127.0.0.1]:54321", want: true},
{name: "remote without port", remoteAddr: "127.0.0.1", want: false},
{name: "unparseable remote", remoteAddr: "not-an-ip", want: false},
{name: "empty remote", remoteAddr: "", want: false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := &http.Request{RemoteAddr: tc.remoteAddr}
require.Equal(t, tc.want, isRequestFromTrustedProxy(req))
})
}
}
+2
View File
@@ -306,6 +306,8 @@
<dd><i class="fa fa{{if .Auth.EnableReverseProxyAutoRegistration}}-check{{end}}-square-o"></i></dd>
<dt>{{.i18n.Tr "admin.config.auth.reverse_proxy_authentication_header"}}</dt>
<dd><code>{{.Auth.ReverseProxyAuthenticationHeader}}</code></dd>
<dt>{{.i18n.Tr "admin.config.auth.trusted_proxy_ips"}}</dt>
<dd><code>{{Join .Auth.TrustedProxyIPs ", "}}</code></dd>
<dt>{{.i18n.Tr "admin.config.auth_custom_logout_url"}}</dt>
<dd>
{{if .Auth.CustomLogoutURL}}