mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-15 13:50:56 +02:00
Addresses a batch of privately reported security issues, grouped by area: - **SSRF** - migration PR-patch/asset fetches, OAuth2 avatar & OpenID discovery, pull-mirror URL re-validation, and the outbound proxy path. - **Access-token scope** - prevent scope escalation on token creation; keep public-only tokens confined (feeds, packages, Actions listings, star/watch lists, limited/private owners). - **Access control / disclosure** - go-get default-branch leak, webhook authorization-header leak, watch clearing on private transitions, label/attachment scoping. - **Denial of service** - input bounds for npm dist-tags, Debian control files, Arch file lists, and SSH keys. ### 📌 Attention for site admins Not breaking - existing configs keep working - but two changes are worth a look: - **New SSRF protection** Outbound requests (migrations, OAuth2 avatars, OpenID discovery, pull mirrors, proxy path) are now validated against the allow/block host lists. If your instance legitimately reaches internal hosts, you may need to add them to `[security].ALLOWED_HOST_LIST` (and the relevant `ALLOW_LOCALNETWORKS` settings). - **Deprecation** `[webhook].ALLOWED_HOST_LIST` is deprecated and will be removed in a future release. Use `[security].ALLOWED_HOST_LIST` instead; the old key still works for now. --------- Co-authored-by: TheFox0x7 <thefox0x7@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Zettat123 <zettat123@gmail.com>
122 lines
4.0 KiB
Go
122 lines
4.0 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package private
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"gitea.dev/modules/httplib"
|
|
"gitea.dev/modules/json"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/proxyprotocol"
|
|
"gitea.dev/modules/setting"
|
|
)
|
|
|
|
// Response is used for internal request response (for user message and error message)
|
|
type Response struct {
|
|
Err string `json:"err,omitempty"` // server-side error log message, it won't be exposed to end users
|
|
UserMsg string `json:"user_msg,omitempty"` // meaningful error message for end users, it will be shown in git client's output.
|
|
}
|
|
|
|
func getClientIP() string {
|
|
sshConnEnv := strings.TrimSpace(os.Getenv("SSH_CONNECTION"))
|
|
if len(sshConnEnv) == 0 {
|
|
return "127.0.0.1"
|
|
}
|
|
return strings.Fields(sshConnEnv)[0]
|
|
}
|
|
|
|
func dialContextInternalAPI(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
|
d := net.Dialer{Timeout: 10 * time.Second}
|
|
if setting.Protocol == setting.HTTPUnix {
|
|
conn, err = d.DialContext(ctx, "unix", setting.HTTPAddr)
|
|
} else {
|
|
conn, err = d.DialContext(ctx, network, address)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if setting.LocalUseProxyProtocol {
|
|
if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
// internalAPIConnectionIsLocal reports whether the internal API transport connects to a local target,
|
|
// where the self-signed local certificate cannot be verified so skipping verification is safe. It mirrors
|
|
// what dialContextInternalAPI actually dials: a unix socket whenever Protocol is HTTPUnix (always local,
|
|
// whatever LOCAL_ROOT_URL says), otherwise the LOCAL_ROOT_URL host directly. A non-loopback LOCAL_ROOT_URL
|
|
// is a real network hop, so its certificate must be verified, else the internal token can be MITM'd. An
|
|
// unparseable LOCAL_ROOT_URL is a hard misconfiguration and fails closed (verify).
|
|
func internalAPIConnectionIsLocal(protocol setting.Scheme, localURL string) bool {
|
|
if protocol == setting.HTTPUnix {
|
|
return true
|
|
}
|
|
u, err := url.Parse(localURL)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
host := u.Hostname()
|
|
if host == "localhost" {
|
|
return true
|
|
}
|
|
ip := net.ParseIP(host)
|
|
return ip != nil && ip.IsLoopback()
|
|
}
|
|
|
|
var internalAPITransport = sync.OnceValue(func() http.RoundTripper {
|
|
return &http.Transport{
|
|
DialContext: dialContextInternalAPI,
|
|
TLSClientConfig: &tls.Config{
|
|
// Skip verification only for a local target (unix socket, or a loopback LOCAL_ROOT_URL), where the
|
|
// self-signed local cert can't be verified anyway; a non-loopback LOCAL_ROOT_URL is a real network
|
|
// hop and must be verified so the internal token can't be MITM'd. When verifying, Go's default
|
|
// ServerName (the dialed LOCAL_ROOT_URL host) is already correct, so it is not overridden.
|
|
InsecureSkipVerify: internalAPIConnectionIsLocal(setting.Protocol, setting.LocalURL),
|
|
},
|
|
}
|
|
})
|
|
|
|
func NewInternalRequest(ctx context.Context, url, method string) *httplib.Request {
|
|
if setting.InternalToken == "" {
|
|
log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q.
|
|
Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf)
|
|
}
|
|
|
|
if !strings.HasPrefix(url, setting.LocalURL) {
|
|
log.Fatal("Invalid internal request URL: %q", url)
|
|
}
|
|
|
|
return httplib.NewRequest(url, method).
|
|
SetContext(ctx).
|
|
SetTransport(internalAPITransport()).
|
|
Header("X-Real-IP", getClientIP()).
|
|
Header("X-Gitea-Internal-Auth", "Bearer "+setting.InternalToken)
|
|
}
|
|
|
|
func newInternalRequestAPI(ctx context.Context, url, method string, body ...any) *httplib.Request {
|
|
req := NewInternalRequest(ctx, url, method)
|
|
if len(body) == 1 {
|
|
req.Header("Content-Type", "application/json")
|
|
jsonBytes, _ := json.Marshal(body[0])
|
|
req.Body(jsonBytes)
|
|
} else if len(body) > 1 {
|
|
log.Fatal("Too many arguments for newInternalRequestAPI")
|
|
}
|
|
|
|
req.SetReadWriteTimeout(60 * time.Second)
|
|
return req
|
|
}
|