diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index dab091e3..8c875008 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -76,10 +76,11 @@ type Rest struct { httpServer *http.Server lock sync.Mutex - pubRest public - privRest private - adminRest admin - rssRest rss + pubRest public + privRest private + adminRest admin + rssRest rss + openRouteLimiter float64 } // LoadingCache defines interface for caching @@ -90,7 +91,7 @@ type LoadingCache interface { } const hardBodyLimit = 1024 * 64 // limit size of body - +const openRouteLimiter = 10 // limit for open routes const lastCommentsScope = "last" type commentsWithInfo struct { @@ -198,6 +199,10 @@ func (s *Rest) makeHTTPServer(address string, port int, router http.Handler) *ht } func (s *Rest) routes() chi.Router { + if s.openRouteLimiter == 0 { + // set the default open route limiter. Just a safety measure as it should be set by Run method anyway + s.openRouteLimiter = openRouteLimiter + } router := chi.NewRouter() router.Use(middleware.Throttle(1000), middleware.RealIP, R.Recoverer(log.Default())) if !s.DisableSignature { @@ -257,7 +262,7 @@ func (s *Rest) routes() chi.Router { // open routes rapi.Group(func(ropen chi.Router) { ropen.Use(middleware.Timeout(30 * time.Second)) - ropen.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil))) + ropen.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(s.openRouteLimiter, nil))) ropen.Use(authMiddleware.Trace, middleware.NoCache, logInfoWithBody) ropen.Get("/config", s.configCtrl) ropen.Get("/find", s.pubRest.findCommentsCtrl) diff --git a/backend/app/rest/api/rest_test.go b/backend/app/rest/api/rest_test.go index badae23e..22e8126e 100644 --- a/backend/app/rest/api/rest_test.go +++ b/backend/app/rest/api/rest_test.go @@ -487,8 +487,9 @@ func startupT(t *testing.T, srvHook ...func(srv *Rest)) (ts *httptest.Server, sr Cache: memCache, KeyStore: astore, }, - NotifyService: notify.NopService, - EmojiEnabled: true, + NotifyService: notify.NopService, + EmojiEnabled: true, + openRouteLimiter: 100, } srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = -5, -10 @@ -504,7 +505,8 @@ func startupT(t *testing.T, srvHook ...func(srv *Rest)) (ts *httptest.Server, sr h(srv) } - ts = httptest.NewServer(srv.routes()) + routes := srv.routes() + ts = httptest.NewServer(routes) teardown = func() { ts.Close() diff --git a/backend/app/rest/proxy/image.go b/backend/app/rest/proxy/image.go index 70972dec..b3062106 100644 --- a/backend/app/rest/proxy/image.go +++ b/backend/app/rest/proxy/image.go @@ -98,6 +98,10 @@ func (p Image) Handler(w http.ResponseWriter, r *http.Request) { if img == nil { img, err = p.downloadImage(context.Background(), imgURL) if err != nil { + if strings.Contains(err.Error(), "invalid content type") { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid content type", rest.ErrImgNotFound) + return + } rest.SendErrorJSON(w, r, http.StatusNotFound, err, "can't get image "+imgURL, rest.ErrAssetNotFound) return } @@ -165,6 +169,11 @@ func (p Image) downloadImage(ctx context.Context, imgURL string) ([]byte, error) return nil, fmt.Errorf("got unsuccessful response status %d while fetching %s", resp.StatusCode, imgURL) } + contentType := resp.Header.Get("Content-Type") + if !strings.HasPrefix(contentType, "image/") { + return nil, fmt.Errorf("invalid content type %s", contentType) + } + imgData, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("unable to read image body") diff --git a/backend/app/rest/proxy/image_test.go b/backend/app/rest/proxy/image_test.go index 10d8543b..0314b243 100644 --- a/backend/app/rest/proxy/image_test.go +++ b/backend/app/rest/proxy/image_test.go @@ -108,27 +108,41 @@ func TestImage_Routes(t *testing.T) { httpSrv := imgHTTPTestsServer(t) defer httpSrv.Close() - encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png")) + t.Run("valid image", func(t *testing.T) { + encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/img1.png")) + resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL) + require.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "1462", resp.Header["Content-Length"][0]) + assert.Equal(t, "image/png", resp.Header["Content-Type"][0]) + }) - resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL) - require.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - assert.Equal(t, http.StatusOK, resp.StatusCode) - assert.Equal(t, "1462", resp.Header["Content-Length"][0]) - assert.Equal(t, "image/png", resp.Header["Content-Type"][0]) + t.Run("no image", func(t *testing.T) { + encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/no-such-image.png")) + resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL) + require.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + }) - encodedImgURL = base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "/image/no-such-image.png")) - resp, err = http.Get(ts.URL + "/?src=" + encodedImgURL) - require.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - assert.Equal(t, http.StatusNotFound, resp.StatusCode) + t.Run("bad encoding", func(t *testing.T) { + encodedImgURL := base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "bad encoding")) + resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL) + require.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + assert.Equal(t, 2, len(imageStore.LoadCalls())) + }) - encodedImgURL = base64.URLEncoding.EncodeToString([]byte(httpSrv.URL + "bad encoding")) - resp, err = http.Get(ts.URL + "/?src=" + encodedImgURL) - require.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) - assert.Equal(t, 2, len(imageStore.LoadCalls())) + t.Run("non-image reference", func(t *testing.T) { + encodedImgURL := base64.URLEncoding.EncodeToString([]byte("https://google.com")) + resp, err := http.Get(ts.URL + "/?src=" + encodedImgURL) + require.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + assert.Equal(t, 3, len(imageStore.LoadCalls())) + }) } func TestImage_DisabledCachingAndHTTP2HTTPS(t *testing.T) {