@marcel91200 / caddy Mirror · read-only / commits / 40b54434
Fork 0

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.