Add fileSize limit to extractors (#35200) (#35280)

Automatic Merge
Этот коммит содержится в:
Mattermost Build
2026-02-13 14:09:32 +02:00
коммит произвёл GitHub
родитель 053dcf62b6
Коммит eb8c99fe9c
12 изменённых файлов: 104 добавлений и 24 удалений

Просмотреть файл

@@ -1587,6 +1587,7 @@ func (a *App) ExtractContentFromFileInfo(rctx request.CTX, fileInfo *model.FileI
defer file.Close() defer file.Close()
text, err := docextractor.Extract(rctx.Logger(), fileInfo.Name, file, docextractor.ExtractSettings{ text, err := docextractor.Extract(rctx.Logger(), fileInfo.Name, file, docextractor.ExtractSettings{
ArchiveRecursion: *a.Config().FileSettings.ArchiveRecursion, ArchiveRecursion: *a.Config().FileSettings.ArchiveRecursion,
MaxFileSize: *a.Config().FileSettings.MaxFileSize,
}) })
if err != nil { if err != nil {
return errors.Wrap(err, "failed to extract file content") return errors.Wrap(err, "failed to extract file content")

Просмотреть файл

@@ -14,6 +14,8 @@ import (
"strings" "strings"
"github.com/mholt/archives" "github.com/mholt/archives"
"github.com/mattermost/mattermost/server/v8/channels/utils"
) )
type archiveExtractor struct { type archiveExtractor struct {
@@ -38,7 +40,7 @@ func getExtAlsoTarGz(name string) string {
return filepath.Ext(name) return filepath.Ext(name)
} }
func (ae *archiveExtractor) Extract(name string, r io.ReadSeeker) (string, error) { func (ae *archiveExtractor) Extract(name string, r io.ReadSeeker, maxFileSize int64) (string, error) {
ext := getExtAlsoTarGz(name) ext := getExtAlsoTarGz(name)
// Create a temporary file, using `*` control the random component while preserving the extension. // Create a temporary file, using `*` control the random component while preserving the extension.
@@ -81,12 +83,19 @@ func (ae *archiveExtractor) Extract(name string, r io.ReadSeeker) (string, error
} }
defer file.Close() defer file.Close()
data, err := io.ReadAll(file) // Limit the size of decompressed archive entries to prevent
if err != nil { // memory exhaustion from zip bombs or other malicious archives.
return err var reader io.Reader = file
if maxFileSize > 0 {
reader = utils.NewLimitedReaderWithError(file, maxFileSize)
} }
subtext, extractErr := ae.SubExtractor.Extract(filename, bytes.NewReader(data)) data, err := io.ReadAll(reader)
if err != nil {
return fmt.Errorf("error reading archive entry %s: %w", path, err)
}
subtext, extractErr := ae.SubExtractor.Extract(filename, bytes.NewReader(data), maxFileSize)
if extractErr == nil { if extractErr == nil {
text.WriteString(subtext + " ") text.WriteString(subtext + " ")
} }

Просмотреть файл

@@ -31,11 +31,11 @@ func (ce *combineExtractor) Match(filename string) bool {
return false return false
} }
func (ce *combineExtractor) Extract(filename string, r io.ReadSeeker) (string, error) { func (ce *combineExtractor) Extract(filename string, r io.ReadSeeker, maxFileSize int64) (string, error) {
for _, extractor := range ce.SubExtractors { for _, extractor := range ce.SubExtractors {
if extractor.Match(filename) { if extractor.Match(filename) {
r.Seek(0, io.SeekStart) r.Seek(0, io.SeekStart)
text, err := extractor.Extract(filename, r) text, err := extractor.Extract(filename, r, maxFileSize)
if err != nil { if err != nil {
ce.logger.Warn("Unable to extract file content", mlog.String("file_name", filename), mlog.String("extractor", extractor.Name()), mlog.Err(err)) ce.logger.Warn("Unable to extract file content", mlog.String("file_name", filename), mlog.String("extractor", extractor.Name()), mlog.Err(err))
continue continue

Просмотреть файл

@@ -12,6 +12,7 @@ import (
// ExtractSettings defines the features enabled/disable during the document text extraction. // ExtractSettings defines the features enabled/disable during the document text extraction.
type ExtractSettings struct { type ExtractSettings struct {
ArchiveRecursion bool ArchiveRecursion bool
MaxFileSize int64
MMPreviewURL string MMPreviewURL string
MMPreviewSecret string MMPreviewSecret string
} }
@@ -44,7 +45,7 @@ func ExtractWithExtraExtractors(logger mlog.LoggerIFace, filename string, r io.R
enabledExtractors.Add(&plainExtractor{}) enabledExtractors.Add(&plainExtractor{})
if enabledExtractors.Match(filename) { if enabledExtractors.Match(filename) {
return enabledExtractors.Extract(filename, r) return enabledExtractors.Extract(filename, r, settings.MaxFileSize)
} }
return "", nil return "", nil
} }

Просмотреть файл

@@ -172,7 +172,7 @@ func (te *customTestPdfExtractor) Match(filename string) bool {
return strings.HasSuffix(filename, ".pdf") return strings.HasSuffix(filename, ".pdf")
} }
func (te *customTestPdfExtractor) Extract(filename string, r io.ReadSeeker) (string, error) { func (te *customTestPdfExtractor) Extract(filename string, r io.ReadSeeker, _ int64) (string, error) {
return "this is a text generated content", nil return "this is a text generated content", nil
} }
@@ -186,7 +186,7 @@ func (te *failingExtractor) Match(filename string) bool {
return true return true
} }
func (te *failingExtractor) Extract(filename string, r io.ReadSeeker) (string, error) { func (te *failingExtractor) Extract(filename string, r io.ReadSeeker, _ int64) (string, error) {
return "", errors.New("this always fail") return "", errors.New("this always fail")
} }
@@ -212,3 +212,72 @@ func TestExtractWithExtraExtractors(t *testing.T) {
assert.Contains(t, text, "contains") assert.Contains(t, text, "contains")
}) })
} }
func TestArchiveMaxFileSize(t *testing.T) {
t.Parallel()
tests := []struct {
name string
file string
recursion bool
limit int64
expectContains []string
expectMissing []string
}{
{
name: "Zip with recursion and large limit extracts fully",
file: "Fake_Team_Import.zip",
recursion: true,
limit: 10 * 1024 * 1024,
expectContains: []string{"purpose", "announcements"},
},
{
name: "Zip with recursion and tiny limit rejects oversized entries",
file: "Fake_Team_Import.zip",
recursion: true,
limit: 1,
expectMissing: []string{"purpose", "announcements"},
},
{
name: "Zip with recursion and zero limit means unlimited",
file: "Fake_Team_Import.zip",
recursion: true,
limit: 0,
expectContains: []string{"purpose", "announcements"},
},
{
name: "Zip without recursion lists paths regardless of limit",
file: "Fake_Team_Import.zip",
recursion: false,
limit: 1,
expectContains: []string{"channels"},
},
{
name: "Tar.gz with recursion and tiny limit rejects oversized entries",
file: "Fake_Team_Import.tar.gz",
recursion: true,
limit: 1,
expectMissing: []string{"purpose", "announcements"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
data, err := testutils.ReadTestFile(tc.file)
require.NoError(t, err)
settings := ExtractSettings{ArchiveRecursion: tc.recursion, MaxFileSize: tc.limit}
text, err := Extract(mlog.CreateConsoleTestLogger(t), tc.file, bytes.NewReader(data), settings)
require.NoError(t, err)
for _, s := range tc.expectContains {
assert.Contains(t, text, s)
}
for _, s := range tc.expectMissing {
assert.NotContains(t, text, s)
}
})
}
}

Просмотреть файл

@@ -36,7 +36,7 @@ func (de *documentExtractor) Match(filename string) bool {
return ok return ok
} }
func (de *documentExtractor) Extract(filename string, r io.ReadSeeker) (out string, outErr error) { func (de *documentExtractor) Extract(filename string, r io.ReadSeeker, _ int64) (out string, outErr error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
out = "" out = ""

Просмотреть файл

@@ -10,6 +10,6 @@ import (
// Extractors define the interface needed to extract file content // Extractors define the interface needed to extract file content
type Extractor interface { type Extractor interface {
Match(filename string) bool Match(filename string) bool
Extract(filename string, file io.ReadSeeker) (string, error) Extract(filename string, file io.ReadSeeker, maxFileSize int64) (string, error)
Name() string Name() string
} }

Просмотреть файл

@@ -45,7 +45,7 @@ func (mpe *mmPreviewExtractor) Match(filename string) bool {
return mmpreviewSupportedExtensions[extension] return mmpreviewSupportedExtensions[extension]
} }
func (mpe *mmPreviewExtractor) Extract(filename string, file io.ReadSeeker) (string, error) { func (mpe *mmPreviewExtractor) Extract(filename string, file io.ReadSeeker, maxFileSize int64) (string, error) {
b, w, err := createMultipartFormData("file", filename, file) b, w, err := createMultipartFormData("file", filename, file)
if err != nil { if err != nil {
return "", errors.Wrap(err, "Unable to generate file preview using mmpreview.") return "", errors.Wrap(err, "Unable to generate file preview using mmpreview.")
@@ -70,7 +70,7 @@ func (mpe *mmPreviewExtractor) Extract(filename string, file io.ReadSeeker) (str
if err != nil { if err != nil {
return "", errors.Wrap(err, "unable to read the response from mmpreview") return "", errors.Wrap(err, "unable to read the response from mmpreview")
} }
return mpe.pdfExtractor.Extract(filename, bytes.NewReader(data)) return mpe.pdfExtractor.Extract(filename, bytes.NewReader(data), maxFileSize)
} }
func createMultipartFormData(fieldName, fileName string, fileData io.ReadSeeker) (bytes.Buffer, *multipart.Writer, error) { func createMultipartFormData(fieldName, fileName string, fileData io.ReadSeeker) (bytes.Buffer, *multipart.Writer, error) {

Просмотреть файл

@@ -29,7 +29,7 @@ func (pe *pdfExtractor) Match(filename string) bool {
return supportedExtensions[extension] return supportedExtensions[extension]
} }
func (pe *pdfExtractor) Extract(filename string, r io.ReadSeeker) (out string, outErr error) { func (pe *pdfExtractor) Extract(filename string, r io.ReadSeeker, _ int64) (out string, outErr error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
out = "" out = ""

Просмотреть файл

@@ -14,7 +14,7 @@ import (
func TestPdfEmptyFile(t *testing.T) { func TestPdfEmptyFile(t *testing.T) {
extractor := pdfExtractor{} extractor := pdfExtractor{}
_, err := extractor.Extract("test.pdf", bytes.NewReader([]byte{})) _, err := extractor.Extract("test.pdf", bytes.NewReader([]byte{}), 0)
require.Error(t, err) require.Error(t, err)
} }
@@ -23,7 +23,7 @@ func TestPdfFile(t *testing.T) {
contentText := "This is a simple document that contains some text." contentText := "This is a simple document that contains some text."
content, err := testutils.ReadTestFile("sample-doc.pdf") content, err := testutils.ReadTestFile("sample-doc.pdf")
require.NoError(t, err) require.NoError(t, err)
extractedText, err := extractor.Extract("sample-doc.pdf", bytes.NewReader(content)) extractedText, err := extractor.Extract("sample-doc.pdf", bytes.NewReader(content), 0)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, contentText, extractedText) require.Equal(t, contentText, extractedText)
} }
@@ -32,6 +32,6 @@ func TestWrongPdfFile(t *testing.T) {
extractor := pdfExtractor{} extractor := pdfExtractor{}
content, err := testutils.ReadTestFile("sample-doc.docx") content, err := testutils.ReadTestFile("sample-doc.docx")
require.NoError(t, err) require.NoError(t, err)
_, err = extractor.Extract("sample-doc.pdf", bytes.NewReader(content)) _, err = extractor.Extract("sample-doc.pdf", bytes.NewReader(content), 0)
require.Error(t, err) require.Error(t, err)
} }

Просмотреть файл

@@ -19,7 +19,7 @@ func (pe *plainExtractor) Match(filename string) bool {
return true return true
} }
func (pe *plainExtractor) Extract(filename string, r io.ReadSeeker) (string, error) { func (pe *plainExtractor) Extract(filename string, r io.ReadSeeker, _ int64) (string, error) {
// This detects any visible character plus any whitespace // This detects any visible character plus any whitespace
validRanges := append(unicode.GraphicRanges, unicode.White_Space) validRanges := append(unicode.GraphicRanges, unicode.White_Space)

Просмотреть файл

@@ -13,7 +13,7 @@ import (
func TestPlainEmptyFile(t *testing.T) { func TestPlainEmptyFile(t *testing.T) {
extractor := plainExtractor{} extractor := plainExtractor{}
extractedText, err := extractor.Extract("test.txt", bytes.NewReader([]byte{})) extractedText, err := extractor.Extract("test.txt", bytes.NewReader([]byte{}), 0)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "", extractedText) require.Equal(t, "", extractedText)
} }
@@ -21,7 +21,7 @@ func TestPlainEmptyFile(t *testing.T) {
func TestPlainTextSmallFile(t *testing.T) { func TestPlainTextSmallFile(t *testing.T) {
extractor := plainExtractor{} extractor := plainExtractor{}
content := strings.Repeat("test \n", 5) content := strings.Repeat("test \n", 5)
extractedText, err := extractor.Extract("test.txt", bytes.NewReader([]byte(content))) extractedText, err := extractor.Extract("test.txt", bytes.NewReader([]byte(content)), 0)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, content, extractedText) require.Equal(t, content, extractedText)
} }
@@ -29,7 +29,7 @@ func TestPlainTextSmallFile(t *testing.T) {
func TestPlainBigFile(t *testing.T) { func TestPlainBigFile(t *testing.T) {
extractor := plainExtractor{} extractor := plainExtractor{}
content := strings.Repeat("test \n", 1000) content := strings.Repeat("test \n", 1000)
extractedText, err := extractor.Extract("test.txt", bytes.NewReader([]byte(content))) extractedText, err := extractor.Extract("test.txt", bytes.NewReader([]byte(content)), 0)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, content, extractedText) require.Equal(t, content, extractedText)
} }
@@ -38,7 +38,7 @@ func TestSmallBinaryFile(t *testing.T) {
extractor := plainExtractor{} extractor := plainExtractor{}
notUTF8Char := byte(0x7) notUTF8Char := byte(0x7)
content := bytes.Repeat([]byte{notUTF8Char}, 1000) content := bytes.Repeat([]byte{notUTF8Char}, 1000)
extractedText, err := extractor.Extract("test.bin", bytes.NewReader(content)) extractedText, err := extractor.Extract("test.bin", bytes.NewReader(content), 0)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "", extractedText) require.Equal(t, "", extractedText)
} }
@@ -47,7 +47,7 @@ func TestBigBinaryFile(t *testing.T) {
extractor := plainExtractor{} extractor := plainExtractor{}
notUTF8Char := byte(0x7) notUTF8Char := byte(0x7)
content := bytes.Repeat([]byte{notUTF8Char}, 10000) content := bytes.Repeat([]byte{notUTF8Char}, 10000)
extractedText, err := extractor.Extract("test.bin", bytes.NewReader(content)) extractedText, err := extractor.Extract("test.bin", bytes.NewReader(content), 0)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "", extractedText) require.Equal(t, "", extractedText)
} }