Skip to content

[MM-69606] Limit decompressed size on archive import - #220

Merged
jgheithcock merged 5 commits into
mainfrom
mm-69606
Jul 13, 2026
Merged

[MM-69606] Limit decompressed size on archive import#220
jgheithcock merged 5 commits into
mainfrom
mm-69606

Conversation

@jgheithcock

@jgheithcock jgheithcock commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Apply the existing importMaxFileSize (70 MB) decompressed-size cap to version.json and archive image entries during import
  • Remove skipped orphan archive entries with the same limit
  • Cap the archive import upload request body with http.MaxBytesReader, matching the file upload handler
  • Fix archive import error responses to use standard API error handling instead of writing raw errors to the response body

Ticket Link

https://mattermost.atlassian.net/browse/MM-69606

Release Note

Fixed an issue where importing a board archive could consume excessive server memory.

Test plan

  • go test ./app/... ./api/... passes
  • TestParseVersionFile — valid version.json accepted; oversized payload rejected
  • TestImportArchiveVersionFileSizeLimit — valid zip archive accepted; oversized version.json entry rejected
  • Regression tests verified to fail when the size-limit fix is reverted
  • Manual: import a normal .boardarchive file via UI or API and confirm success

Change Impact: 🟠 Medium

Regression Risk: Medium—this changes user-facing archive import behavior by enforcing stricter request/body and per-entry decompressed size limits, adjusts ZIP stream handling for orphan entries (now actively discarded), and standardizes API error mapping (400/413). While blast radius is largely limited to archive import, it’s a core business flow with behavior-contract changes that can affect edge-case imports and cleanup.

QA Recommendation: Limited manual QA recommended: validate a successful small-to-medium archive import, verify 413 for oversized uploads and correct JSON/content-type responses, verify 400 for non-multipart bodies, and confirm cleanup/absence of partially imported oversized assets for both board-mapped entries and orphan entries.
Generated by CodeRabbitAI

parseVersionFile and archive image imports read zip entries without
bounds, allowing small compressed payloads to allocate large amounts
of memory. Apply the existing importMaxFileSize cap consistently across
all entry types and cap the upload request body via MaxBytesReader.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b63eb34a-818c-45dc-8e69-12e51a3f52a8

📥 Commits

Reviewing files that changed from the base of the PR and between f3f9c2e and f82b6d6.

📒 Files selected for processing (1)
  • server/app/import_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/app/import_test.go

📝 Walkthrough

Walkthrough

Adds configured size limits to archive import requests and ZIP entries, maps oversized or malformed uploads to structured errors, and cleans up partial oversized saves. It also adds multipart client support and expands application, API, and integration coverage.

Changes

Archive import size enforcement

Layer / File(s) Summary
HTTP archive upload limits
server/api/archive.go, server/api/archive_test.go
handleArchiveImport caps configured request bodies and maps size-limit and malformed multipart errors to HTTP 413 or 400 JSON responses.
Bounded ZIP entry reads
server/app/import.go
Applies effective limits to version metadata, orphan entries, and board files, removing partially saved files when an entry exceeds the limit.
Archive entry size-limit coverage
server/app/import_test.go
Tests configured limits, bounded version parsing, oversized version/image/orphan entries, and cleanup after partial saves.

Multipart upload integration

Layer / File(s) Summary
Multipart client and integration coverage
server/client/client.go, server/integrationtests/...
Adds single-file multipart POST support and updates archive import integration cases to use multipart requests while testing oversized and malformed uploads.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MultipartClient
  participant ArchiveAPI
  participant ImportArchive
  participant FileBackend
  Client->>MultipartClient: Build multipart archive POST
  MultipartClient->>ArchiveAPI: Send bounded upload
  ArchiveAPI->>ImportArchive: Parse archive request
  ImportArchive->>FileBackend: Save bounded ZIP entries
  FileBackend-->>ImportArchive: Return save result
  ImportArchive-->>ArchiveAPI: Return success or size-limit error
  ArchiveAPI-->>Client: Return JSON HTTP response
Loading

Suggested reviewers: edgarbellot, nang2049

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: enforcing size limits during archive import.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mm-69606

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
server/app/import.go (1)

95-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated "+1 sentinel / N<=0" bounded-read pattern across three sites.

The same "wrap in io.LimitedReader{N: limit+1}, then check N<=0" logic is repeated inline (fileReader), in discardLimited, and in parseVersionFile. Extracting a small shared helper (e.g. one that returns a *io.LimitedReader and a companion exceeded() check) would reduce the chance of the off-by-one sentinel being applied inconsistently in future edits.

Also applies to: 100-107, 479-494, 496-505

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/app/import.go` around lines 95 - 99, The bounded-read logic is
duplicated across fileReader, discardLimited, and parseVersionFile, including
the repeated limit+1 sentinel and N<=0 check. Refactor this pattern into a small
shared helper used by fileReader, discardLimited, and parseVersionFile so they
all construct the same io.LimitedReader and use one companion exceeded check
consistently. Keep the existing behavior intact while centralizing the
off-by-one handling to avoid future drift.
server/api/archive.go (2)

160-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing test coverage for the new size-limit-to-413 mapping.

The ZIP-entry-level limits gained tests in import_test.go, but this handler-level MaxBytesReader/413-mapping change has no accompanying test. Consider adding a handler test that simulates an oversized multipart body and asserts a model.ErrRequestEntityTooLarge response.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/archive.go` around lines 160 - 172, The archive upload handler in
a.app’s request flow maps http.MaxBytesReader failures to
model.ErrRequestEntityTooLarge, but there is no test covering this path. Add a
handler test around the UploadFormFileKey multipart parsing in archive.go that
sends an oversized body with MaxFileSize enabled and verifies errorResponse
returns model.ErrRequestEntityTooLarge rather than a generic bad request.

166-170: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use http.MaxBytesError instead of matching the error string. The module targets Go 1.24.6, so errors.As(err, &maxBytesErr) is available here and is safer than strings.HasSuffix(err.Error(), "http: request body too large"), which can break if the message changes or gets wrapped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/archive.go` around lines 166 - 170, The request-body size check in
archive handling should not rely on matching the error string; update the error
handling in the archive endpoint flow to detect `http.MaxBytesError` using
`errors.As` before falling back to `model.NewErrBadRequest`. Keep the existing
`a.errorResponse` behavior in the same branch, but use the typed error path in
the relevant handler function so oversized uploads map to
`model.ErrRequestEntityTooLarge` safely even if the message is wrapped or
changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/app/import.go`:
- Around line 100-107: The oversized import path in aSaveFile handling currently
returns errSizeLimitExceeded without cleaning up the partially written object.
Update the import flow in import.go, using the SaveFile result (newFileName) and
the fileBackend RemoveFile(path string) method, to delete the saved file
whenever fileReader.N indicates the size limit was exceeded before returning the
error.

---

Nitpick comments:
In `@server/api/archive.go`:
- Around line 160-172: The archive upload handler in a.app’s request flow maps
http.MaxBytesReader failures to model.ErrRequestEntityTooLarge, but there is no
test covering this path. Add a handler test around the UploadFormFileKey
multipart parsing in archive.go that sends an oversized body with MaxFileSize
enabled and verifies errorResponse returns model.ErrRequestEntityTooLarge rather
than a generic bad request.
- Around line 166-170: The request-body size check in archive handling should
not rely on matching the error string; update the error handling in the archive
endpoint flow to detect `http.MaxBytesError` using `errors.As` before falling
back to `model.NewErrBadRequest`. Keep the existing `a.errorResponse` behavior
in the same branch, but use the typed error path in the relevant handler
function so oversized uploads map to `model.ErrRequestEntityTooLarge` safely
even if the message is wrapped or changes.

In `@server/app/import.go`:
- Around line 95-99: The bounded-read logic is duplicated across fileReader,
discardLimited, and parseVersionFile, including the repeated limit+1 sentinel
and N<=0 check. Refactor this pattern into a small shared helper used by
fileReader, discardLimited, and parseVersionFile so they all construct the same
io.LimitedReader and use one companion exceeded check consistently. Keep the
existing behavior intact while centralizing the off-by-one handling to avoid
future drift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 49c26b08-514c-40df-80c6-77c37aacfe6f

📥 Commits

Reviewing files that changed from the base of the PR and between 162e5a5 and c353c9e.

📒 Files selected for processing (3)
  • server/api/archive.go
  • server/app/import.go
  • server/app/import_test.go

Comment thread server/app/import.go Outdated
Clean up partial files on size-limit errors, use typed MaxBytesError handling, and add coverage for image/orphan limits and API 413/400 responses. Update permissions and integration tests to send valid multipart uploads after the handler started rejecting non-multipart bodies.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
server/integrationtests/import_archive_test.go (2)

35-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider asserting the actual error response body, not just headers.

Both subtests only check status code and Content-Type; neither decodes the body to confirm it's a well-formed standard API error object (e.g. has an id/message), which is the core behavior this PR changes (raw error write → errorResponse).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/integrationtests/import_archive_test.go` around lines 35 - 84, The
import archive error tests only verify status and Content-Type, but they should
also confirm the response body is the expected JSON API error object. In
TestImportArchiveAPIErrors, update both subtests to parse the body returned by
ImportArchive/DoAPIPost and assert standard error fields such as id and message,
using the existing test helpers and responses from th.Client and setupClients.
This will validate the errorResponse behavior instead of only checking headers.

54-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse minimalImportArchiveZip instead of duplicating zip construction.

♻️ Suggested fix
-		var buf bytes.Buffer
-		zw := zip.NewWriter(&buf)
-		w, err := zw.Create("version.json")
-		require.NoError(t, err)
-		_, err = w.Write([]byte(`{"version":2}`))
-		require.NoError(t, err)
-		require.NoError(t, zw.Close())
-
-		resp := th.Client.ImportArchive(teamID, bytes.NewReader(buf.Bytes()))
+		resp := th.Client.ImportArchive(teamID, bytes.NewReader(minimalImportArchiveZip(t)))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/integrationtests/import_archive_test.go` around lines 54 - 60, The
test is manually rebuilding the same minimal import archive zip contents instead
of reusing the shared helper. Replace the inline zip construction in this test
with a call to minimalImportArchiveZip so the archive setup stays centralized
and consistent. Keep the test focused on its assertions and use the existing
helper as the single source for creating the version.json-based archive.
server/client/client.go (1)

100-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate multipart-upload logic.

This duplicates the multipart-building code already present in ImportArchive and TeamUploadFile in this same file, differing only in field/file names.

♻️ Suggested consolidation
+func (c *Client) doMultipartPost(url, fieldName, fileName string, data io.Reader) (*http.Response, error) {
+	body := &bytes.Buffer{}
+	writer := multipart.NewWriter(body)
+	part, err := writer.CreateFormFile(fieldName, fileName)
+	if err != nil {
+		return nil, err
+	}
+	if _, err = io.Copy(part, data); err != nil {
+		return nil, err
+	}
+	if err = writer.Close(); err != nil {
+		return nil, err
+	}
+
+	opt := func(r *http.Request) {
+		r.Header.Set("Content-Type", writer.FormDataContentType())
+	}
+
+	return c.doAPIRequestReader(http.MethodPost, c.APIURL+url, body, "", opt)
+}
+
 func (c *Client) DoAPIPostMultipart(url, fieldName, fileName string, data []byte) (*http.Response, error) {
-	body := &bytes.Buffer{}
-	writer := multipart.NewWriter(body)
-	part, err := writer.CreateFormFile(fieldName, fileName)
-	if err != nil {
-		return nil, err
-	}
-	if _, err = part.Write(data); err != nil {
-		return nil, err
-	}
-	if err = writer.Close(); err != nil {
-		return nil, err
-	}
-
-	opt := func(r *http.Request) {
-		r.Header.Set("Content-Type", writer.FormDataContentType())
-	}
-
-	return c.doAPIRequestReader(http.MethodPost, c.APIURL+url, body, "", opt)
+	return c.doMultipartPost(url, fieldName, fileName, bytes.NewReader(data))
 }

ImportArchive and TeamUploadFile could similarly delegate to doMultipartPost.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/client/client.go` around lines 100 - 119, The multipart upload logic
in DoAPIPostMultipart is duplicated with the existing multipart-building flow
used by ImportArchive and TeamUploadFile. Refactor by centralizing the shared
request construction into doMultipartPost and make DoAPIPostMultipart delegate
to it, passing through the field name, file name, and payload. Then update
ImportArchive and TeamUploadFile to use the same helper so all multipart POST
paths share one implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@server/client/client.go`:
- Around line 100-119: The multipart upload logic in DoAPIPostMultipart is
duplicated with the existing multipart-building flow used by ImportArchive and
TeamUploadFile. Refactor by centralizing the shared request construction into
doMultipartPost and make DoAPIPostMultipart delegate to it, passing through the
field name, file name, and payload. Then update ImportArchive and TeamUploadFile
to use the same helper so all multipart POST paths share one implementation.

In `@server/integrationtests/import_archive_test.go`:
- Around line 35-84: The import archive error tests only verify status and
Content-Type, but they should also confirm the response body is the expected
JSON API error object. In TestImportArchiveAPIErrors, update both subtests to
parse the body returned by ImportArchive/DoAPIPost and assert standard error
fields such as id and message, using the existing test helpers and responses
from th.Client and setupClients. This will validate the errorResponse behavior
instead of only checking headers.
- Around line 54-60: The test is manually rebuilding the same minimal import
archive zip contents instead of reusing the shared helper. Replace the inline
zip construction in this test with a call to minimalImportArchiveZip so the
archive setup stays centralized and consistent. Keep the test focused on its
assertions and use the existing helper as the single source for creating the
version.json-based archive.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8be52b65-bf33-4660-82ae-63454d773cb1

📥 Commits

Reviewing files that changed from the base of the PR and between c353c9e and bde292a.

📒 Files selected for processing (7)
  • server/api/archive.go
  • server/api/archive_test.go
  • server/app/import.go
  • server/app/import_test.go
  • server/client/client.go
  • server/integrationtests/import_archive_test.go
  • server/integrationtests/permissions_test.go
✅ Files skipped from review due to trivial changes (1)
  • server/api/archive_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/app/import.go
  • server/app/import_test.go

@jgheithcock jgheithcock added 2: Dev Review Requires review by a core committer 3: QA Review Requires review by a QA tester 3: Security Review Review requested from Security Team labels Jul 2, 2026
@jgheithcock jgheithcock removed the 3: QA Review Requires review by a QA tester label Jul 2, 2026
@nang2049

nang2049 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Hey @jgheithcock I have a branch for MM-69607 that overlaps a lot with this #218.

Same:
handleArchiveImport: http.MaxBytesReader(MaxFileSize) and 413/400 error handling

Different:
You cap per-entry reads at importMaxFileSize (70 MB) inside ImportArchive and my cap writes at a.config.MaxFileSize (300 MB) inside SaveFile which applies to every caller to take into account for future upload paths.

Your 70 MB per-entry cap already covers the disk-exhaustion scenario from MM-69607 so my archive specific changes are redundant with yours.

How do you want to reconcile? I close mine and you land as is or merge my SaveFile cap into this PR.

@nang2049

nang2049 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@edgarbellot how do you think we can reconcile both changes?

@jgheithcock

Copy link
Copy Markdown
Contributor Author

@nang2049 - IIUC, your PR handles my situation, but with configs that I prefer, so I vote to close this one. @edgarbellot , any objection?

@jgheithcock jgheithcock closed this Jul 6, 2026
@nang2049

nang2049 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

@jgheithcock can we reopen this? we need to see how to reconcile both PR

@nang2049 nang2049 reopened this Jul 8, 2026
@jgheithcock

Copy link
Copy Markdown
Contributor Author

@nang2049 - what is the plan for reconciling these?

Comment thread server/app/import.go Outdated
continue
}
newFileName, err := a.SaveFile(zr, opt.TeamID, board.ID, filename, board.IsTemplate)
fileReader := newLimitedReader(zr, importMaxFileSize)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jgheithcock @nang2049 This PR correctly fixes both issues. I think we can apply the remaining change described below and close PR #218.

The gap worth closing before merging: importMaxFileSize is a hardcoded 70 MB constant and doesn't respect the server's configured MaxFileSize. If an admin sets MaxFileSize below 70 MB, archive imports can still write up to 70 MB per entry.

The fix is straightforward - derive the effective cap from both values wherever importMaxFileSize is used (L95, L100, L497):

maxEntry := importMaxFileSize
if cfgMax := a.app.GetConfig().MaxFileSize; cfgMax > 0 && cfgMax < maxEntry {
    maxEntry = cfgMax
}
fileReader := newLimitedReader(zr, maxEntry)

@nang2049
nang2049 requested a review from edgarbellot July 13, 2026 12:21
Nevyana Angelova added 2 commits July 13, 2026 15:24

@edgarbellot edgarbellot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! We can close #218 since this PR addresses both.

@edgarbellot edgarbellot removed the 3: Security Review Review requested from Security Team label Jul 13, 2026
@jgheithcock
jgheithcock merged commit a807c9e into main Jul 13, 2026
26 of 30 checks passed
@avasconcelos114

Copy link
Copy Markdown
Member

/cherry-pick release-9.3

@mattermost-build

Copy link
Copy Markdown
Contributor

Cherry pick is scheduled.

avasconcelos114 pushed a commit that referenced this pull request Jul 16, 2026
* [MM-69606] Limit decompressed size when importing board archives.

parseVersionFile and archive image imports read zip entries without
bounds, allowing small compressed payloads to allocate large amounts
of memory. Apply the existing importMaxFileSize cap consistently across
all entry types and cap the upload request body via MaxBytesReader.

* [MM-69606] Address review feedback and fix archive import tests.

Clean up partial files on size-limit errors, use typed MaxBytesError handling, and add coverage for image/orphan limits and API 413/400 responses. Update permissions and integration tests to send valid multipart uploads after the handler started rejecting non-multipart bodies.

* Honor configured MaxFileSize when stricter than importMaxFileSize

* fix linter

* tets

---------


(cherry picked from commit a807c9e)

Co-authored-by: JG Heithcock <jgheithcock@gmail.com>
Co-authored-by: Nevyana Angelova <nevyangelova@192.168.1.10>
@avasconcelos114

Copy link
Copy Markdown
Member

/cherry-pick release-9.2

@mattermost-build

Copy link
Copy Markdown
Contributor

Cherry pick is scheduled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2: Dev Review Requires review by a core committer CherryPick/Done

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants