Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 13, 21:45 UTC
admin: Enforce and refactor origin checking
Using URLs seems a little cleaner and more correct cf: https://caddy.community/t/protect-admin-endpoint/15114 (This used to work. Something must have changed recently.)
Matthew Holt committed
Feb 15, 2022 at 12:08 UTC
40b54434f3cdb804ef10eee0ba5d8d6c390e93d4
1 file changed
+58
-27
admin.go
+58
-27
@@ -42,6 +42,7 @@ import (
42
"github.com/caddyserver/certmagic"
43
"github.com/prometheus/client_golang/prometheus"
44
"go.uber.org/zap"
45
+ "go.uber.org/zap/zapcore"
46
)
47
48
// AdminConfig configures Caddy's API endpoint, which is used
@@ -192,6 +193,7 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admin
193
} else {
194
muxWrap.enforceHost = !addr.isWildcardInterface()
195
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
196
+ muxWrap.enforceOrigin = admin.EnforceOrigin
197
}
198
199
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
@@ -252,7 +254,7 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admin
254
// will be used as the default origin. If admin.Origins is
255
// empty, no origins will be allowed, effectively bricking the
256
// endpoint for non-unix-socket endpoints, but whatever.
255
-func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
257
+func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
258
uniqueOrigins := make(map[string]struct{})
259
for _, o := range admin.Origins {
260
uniqueOrigins[o] = struct{}{}
@@ -276,8 +278,23 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
278
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
279
}
280
}
279
- allowed := make([]string, 0, len(uniqueOrigins))
280
- for origin := range uniqueOrigins {
281
+ allowed := make([]*url.URL, 0, len(uniqueOrigins))
282
+ for originStr := range uniqueOrigins {
283
+ var origin *url.URL
284
+ if strings.Contains(originStr, "://") {
285
+ var err error
286
+ origin, err = url.Parse(originStr)
287
+ if err != nil {
288
+ continue
289
+ }
290
+ origin.Path = ""
291
+ origin.RawPath = ""
292
+ origin.Fragment = ""
293
+ origin.RawFragment = ""
294
+ origin.RawQuery = ""
295
+ } else {
296
+ origin = &url.URL{Host: originStr}
297
+ }
298
allowed = append(allowed, origin)
299
}
300
return allowed
@@ -358,7 +375,7 @@ func replaceLocalAdminServer(cfg *Config) error {
375
adminLogger.Info("admin endpoint started",
376
zap.String("address", addr.String()),
377
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
361
- zap.Strings("origins", handler.allowedOrigins))
378
+ zap.Array("origins", loggableURLArray(handler.allowedOrigins)))
379
380
if !handler.enforceHost {
381
adminLogger.Warn("admin endpoint on open interface; host checking disabled",
@@ -650,10 +667,10 @@ type AdminRoute struct {
667
type adminHandler struct {
668
mux *http.ServeMux
669
653
- // security for local/plaintext) endpoint, on by default
670
+ // security for local/plaintext endpoint
671
enforceOrigin bool
672
enforceHost bool
656
- allowedOrigins []string
673
+ allowedOrigins []*url.URL
674
675
// security for remote/encrypted endpoint
676
remoteControl *RemoteAdmin
@@ -779,8 +796,8 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
796
// rebinding attacks.
797
func (h adminHandler) checkHost(r *http.Request) error {
798
var allowed bool
782
- for _, allowedHost := range h.allowedOrigins {
783
- if r.Host == allowedHost {
799
+ for _, allowedOrigin := range h.allowedOrigins {
800
+ if r.Host == allowedOrigin.Host {
801
allowed = true
802
break
803
}
@@ -799,43 +816,45 @@ func (h adminHandler) checkHost(r *http.Request) error {
816
// sites from issuing requests to our listener. It
817
// returns the origin that was obtained from r.
818
func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
802
- origin := h.getOriginHost(r)
803
- if origin == "" {
804
- return origin, APIError{
819
+ originStr, origin := h.getOrigin(r)
820
+ if origin == nil {
821
+ return "", APIError{
822
HTTPStatus: http.StatusForbidden,
806
- Err: fmt.Errorf("missing required Origin header"),
823
+ Err: fmt.Errorf("required Origin header is missing or invalid"),
824
}
825
}
826
if !h.originAllowed(origin) {
810
- return origin, APIError{
827
+ return "", APIError{
828
HTTPStatus: http.StatusForbidden,
812
- Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
829
+ Err: fmt.Errorf("client is not allowed to access from origin '%s'", originStr),
830
}
831
}
815
- return origin, nil
832
+ return origin.String(), nil
833
}
834
818
-func (h adminHandler) getOriginHost(r *http.Request) string {
835
+func (h adminHandler) getOrigin(r *http.Request) (string, *url.URL) {
836
origin := r.Header.Get("Origin")
837
if origin == "" {
838
origin = r.Header.Get("Referer")
839
}
840
originURL, err := url.Parse(origin)
824
- if err == nil && originURL.Host != "" {
825
- origin = originURL.Host
826
- }
827
- return origin
841
+ if err != nil {
842
+ return origin, nil
843
+ }
844
+ originURL.Path = ""
845
+ originURL.RawPath = ""
846
+ originURL.Fragment = ""
847
+ originURL.RawFragment = ""
848
+ originURL.RawQuery = ""
849
+ return origin, originURL
850
}
851
830
-func (h adminHandler) originAllowed(origin string) bool {
852
+func (h adminHandler) originAllowed(origin *url.URL) bool {
853
for _, allowedOrigin := range h.allowedOrigins {
832
- originCopy := origin
833
- if !strings.Contains(allowedOrigin, "://") {
834
- // no scheme specified, so allow both
835
- originCopy = strings.TrimPrefix(originCopy, "http://")
836
- originCopy = strings.TrimPrefix(originCopy, "https://")
854
+ if allowedOrigin.Scheme != "" && origin.Scheme != allowedOrigin.Scheme {
855
+ continue
856
}
838
- if originCopy == allowedOrigin {
857
+ if origin.Host == allowedOrigin.Host {
858
return true
859
}
860
}
@@ -1189,6 +1208,18 @@ func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
1208
return x509.ParseCertificate(derBytes)
1209
}
1210
1211
+type loggableURLArray []*url.URL
1212
+
1213
+func (ua loggableURLArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
1214
+ if ua == nil {
1215
+ return nil
1216
+ }
1217
+ for _, u := range ua {
1218
+ enc.AppendString(u.String())
1219
+ }
1220
+ return nil
1221
+}
1222
+
1223
var (
1224
// DefaultAdminListen is the address for the local admin
1225
// listener, if none is specified at startup.