From d8ca4b67525f3d656bd5a917af52d5849bd23d94 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Fri, 9 Oct 2026 21:01:20 +0200 Subject: [PATCH] fix: limit LDAP profile picture downloads to 2 MB --- backend/internal/ldapsync/service.go | 12 +++++++-- backend/internal/ldapsync/service_test.go | 33 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/backend/internal/ldapsync/service.go b/backend/internal/ldapsync/service.go index 0c0cea0a..585feacf 100644 --- a/backend/internal/ldapsync/service.go +++ b/backend/internal/ldapsync/service.go @@ -5,6 +5,7 @@ import ( "context" "crypto/tls" "encoding/base64" + "errors" "fmt" "io" "log/slog" @@ -756,8 +757,15 @@ func (s *Service) saveProfilePicture(parentCtx context.Context, userId string, p } defer res.Body.Close() - data, err := io.ReadAll(res.Body) - if err != nil { + const maxProfilePictureSize int64 = 2 * 1024 * 1024 // 2MB + if res.ContentLength > maxProfilePictureSize { + return fmt.Errorf("profile picture must not exceed 2 MB") + } + + data, err := io.ReadAll(utils.NewLimitReader(res.Body, maxProfilePictureSize+1)) + if errors.Is(err, utils.ErrSizeExceeded) { + return fmt.Errorf("profile picture must not exceed 2 MB") + } else if err != nil { return fmt.Errorf("failed to read profile picture: %w", err) } diff --git a/backend/internal/ldapsync/service_test.go b/backend/internal/ldapsync/service_test.go index a40f2db9..ed1a36b0 100644 --- a/backend/internal/ldapsync/service_test.go +++ b/backend/internal/ldapsync/service_test.go @@ -1,7 +1,9 @@ package ldapsync import ( + "bytes" "net/http" + "net/http/httptest" "testing" "github.com/go-ldap/ldap/v3" @@ -45,6 +47,37 @@ func (c *fakeLDAPClient) Close() error { return nil } +func TestSaveProfilePictureRejectsOversizedDownload(t *testing.T) { + oversized := bytes.Repeat([]byte{0}, 2*1024*1024+1) + + // Cover both a declared Content-Length and a chunked response that hides its size until it is read + tests := []struct { + name string + chunked bool + }{ + {name: "content length", chunked: false}, + {name: "chunked", chunked: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if tt.chunked { + w.(http.Flusher).Flush() + } + _, _ = w.Write(oversized) + })) + t.Cleanup(server.Close) + + svc := newService(Dependencies{HTTPClient: server.Client()}) + + err := svc.saveProfilePicture(t.Context(), "user-id", server.URL+"/picture.png") + + require.ErrorContains(t, err, "must not exceed 2 MB") + }) + } +} + func TestLdapServiceSyncAllReconcilesUsersAndGroups(t *testing.T) { service, db := newTestLdapService(t, newFakeLDAPClient( ldapSearchResult(