diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal.tsx index 8f11c7930e5..738f2f502e0 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal.tsx @@ -5,6 +5,7 @@ import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import { IAppStoreApp, ISoftwarePackage } from "interfaces/software"; import { NotificationContext } from "context/notification"; +import { getErrorReason } from "interfaces/errors"; import softwareAPI from "services/entities/software"; import Modal from "components/Modal"; @@ -529,7 +530,8 @@ const EditIconModal = ({ setIconUploadedAt(new Date().toISOString()); onExitEditIconModal(); } catch (e) { - renderFlash("error", DEFAULT_ERROR_MESSAGE); + const errorMessage = getErrorReason(e) || DEFAULT_ERROR_MESSAGE; + renderFlash("error", errorMessage); } finally { setIsUpdatingIcon(false); } diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 6e05b3c1a70..e815eb04986 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -12226,6 +12226,47 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareTitleIcons() { require.Equal(t, iconUrl, *details.SoftwareIconURL) require.Equal(t, titleID, details.SoftwareTitleID) + // PUT: Icon too large + // Create a fake large file (101KB of data) + largeData := make([]byte, 101*1024) + reader := bytes.NewReader(largeData) + iconFile, err := fleet.NewTempFileReader(reader, func() string { return t.TempDir() }) + require.NoError(t, err) + + var bufInvalid bytes.Buffer + writer = multipart.NewWriter(&bufInvalid) + fileWriter, err = writer.CreateFormFile("icon", "icon.png") + require.NoError(t, err) + _, err = io.Copy(fileWriter, iconFile) + require.NoError(t, err) + err = writer.Close() + require.NoError(t, err) + headers = map[string]string{ + "Content-Type": writer.FormDataContentType(), + "Authorization": fmt.Sprintf("Bearer %s", s.token), + } + resp = s.DoRawWithHeaders( + "PUT", + fmt.Sprintf("/api/latest/fleet/software/titles/%d/icon?team_id=%d", titleID, tm.ID), + bufInvalid.Bytes(), + http.StatusBadRequest, + headers, + ) + var errorResp struct { + Message string `json:"message"` + Errors []struct { + Name string `json:"name"` + Reason string `json:"reason"` + } `json:"errors"` + } + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + err = json.Unmarshal(body, &errorResp) + require.NoError(t, err) + assert.Equal(t, "Bad request", errorResp.Message) + require.Len(t, errorResp.Errors, 1) + assert.Contains(t, errorResp.Errors[0].Reason, "icon must be less than 100KB") + // PUT: gitops workflow, passing in sha256 & filename var storedIcons []fleet.SoftwareTitleIcon mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { diff --git a/server/service/software_title_icons.go b/server/service/software_title_icons.go index 74297f04119..bca6551d63c 100644 --- a/server/service/software_title_icons.go +++ b/server/service/software_title_icons.go @@ -171,6 +171,19 @@ func (putSoftwareTitleIconRequest) DecodeRequest(ctx context.Context, r *http.Re } } + // Validate the file if one is provided + if decoded.File != nil { + file, err := decoded.File.Open() + if err != nil { + return nil, &fleet.BadRequestError{Message: "failed to open file"} + } + defer file.Close() + + if err := iconValidator(file); err != nil { + return nil, err + } + } + return &decoded, nil } @@ -189,12 +202,6 @@ func putSoftwareTitleIconEndpoint(ctx context.Context, request interface{}, svc } defer file.Close() - // ensure icon is png and fits sizing restrictions - err = iconValidator(file) - if err != nil { - return putSoftwareTitleIconResponse{Err: err}, nil - } - tfr, err := fleet.NewTempFileReader(file, nil) if err != nil { return putSoftwareTitleIconResponse{Err: err}, nil @@ -228,6 +235,20 @@ func (svc *Service) UploadSoftwareTitleIcon(ctx context.Context, payload *fleet. } func iconValidator(file multipart.File) error { + // Check file size first + fileSize, err := file.Seek(0, 2) // Seek to end to get size + if err != nil { + return &fleet.BadRequestError{Message: "failed to read file size"} + } + if _, err := file.Seek(0, 0); err != nil { // Reset to beginning + return &fleet.BadRequestError{Message: "failed to rewind file"} + } + + maxSize := int64(100 * 1024) // 100KB + if fileSize > maxSize { + return &fleet.BadRequestError{Message: "icon must be less than 100KB"} + } + config, format, err := image.DecodeConfig(file) if err != nil || format != "png" { return &fleet.BadRequestError{Message: "icon must be a PNG image"}