Files
94e2325540 [MM-65822] Add directory conflict checks for plugin and import uploads (#34681)
* [MM-66789] Add directory conflict checks for plugin and import uploads

Prevent security issues where plugin uploads could write into the import
directory (or subdirectories) and vice versa by adding validation checks
at the REST API level when uploading plugins or creating import uploads.

* improved handling of root directories

---------

Co-authored-by: Mattermost Build <[email protected]>
2026-03-03 16:31:05 -04:00

308 lines
7.9 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package fileutils
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFindFile(t *testing.T) {
t.Run("files from various paths", func(t *testing.T) {
// Create the following directory structure:
// tmpDir1/
// file1.json
// file2.xml
// other.txt
// tmpDir2/
// other.txt/ [directory]
// tmpDir3/
// tmpDir4/
// tmpDir5/
tmpDir1, err := os.MkdirTemp("", "")
require.NoError(t, err)
defer func() {
err = os.RemoveAll(tmpDir1)
require.NoError(t, err)
}()
tmpDir2, err := os.MkdirTemp(tmpDir1, "")
require.NoError(t, err)
err = os.Mkdir(filepath.Join(tmpDir2, "other.txt"), 0700)
require.NoError(t, err)
tmpDir3, err := os.MkdirTemp(tmpDir2, "")
require.NoError(t, err)
tmpDir4, err := os.MkdirTemp(tmpDir3, "")
require.NoError(t, err)
tmpDir5, err := os.MkdirTemp(tmpDir4, "")
require.NoError(t, err)
type testCase struct {
Description string
Cwd *string
FileName string
Expected string
}
testCases := []testCase{}
for _, fileName := range []string{"file1.json", "file2.xml", "other.txt"} {
filePath := filepath.Join(tmpDir1, fileName)
err = os.WriteFile(filePath, []byte("{}"), 0600)
require.NoError(t, err)
// Relative paths end up getting symlinks fully resolved, so use this below as necessary.
filePathResolved, err := filepath.EvalSymlinks(filePath)
require.NoError(t, err)
testCases = append(testCases, []testCase{
{
fmt.Sprintf("absolute path to %s", fileName),
nil,
filePath,
filePath,
},
{
fmt.Sprintf("absolute path to %s from containing directory", fileName),
&tmpDir1,
filePath,
filePath,
},
{
fmt.Sprintf("relative path to %s from containing directory", fileName),
&tmpDir1,
fileName,
filePathResolved,
},
{
fmt.Sprintf("%s: subdirectory of containing directory", fileName),
&tmpDir2,
fileName,
filePathResolved,
},
{
fmt.Sprintf("%s: twice-nested subdirectory of containing directory", fileName),
&tmpDir3,
fileName,
filePathResolved,
},
{
fmt.Sprintf("%s: thrice-nested subdirectory of containing directory", fileName),
&tmpDir4,
fileName,
filePathResolved,
},
{
fmt.Sprintf("%s: quadruple-nested subdirectory of containing directory", fileName),
&tmpDir5,
fileName,
filePathResolved,
},
}...)
}
for _, testCase := range testCases {
t.Run(testCase.Description, func(t *testing.T) {
if testCase.Cwd != nil {
prevDir, err := os.Getwd()
require.NoError(t, err)
err = os.Chdir(*testCase.Cwd)
require.NoError(t, err)
defer func() {
err = os.Chdir(prevDir)
require.NoError(t, err)
}()
}
assert.Equal(t, testCase.Expected, FindFile(testCase.FileName))
})
}
})
}
func TestCheckDirectoryConflict(t *testing.T) {
t.Run("separate directories", func(t *testing.T) {
tmpDir1, err := os.MkdirTemp("", "dir1")
require.NoError(t, err)
defer os.RemoveAll(tmpDir1)
tmpDir2, err := os.MkdirTemp("", "dir2")
require.NoError(t, err)
defer os.RemoveAll(tmpDir2)
conflict, err := CheckDirectoryConflict(tmpDir1, tmpDir2)
require.NoError(t, err)
assert.False(t, conflict)
})
t.Run("same directory", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "samedir")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
conflict, err := CheckDirectoryConflict(tmpDir, tmpDir)
require.NoError(t, err)
assert.True(t, conflict)
})
t.Run("first is subdirectory of second", func(t *testing.T) {
tmpDir1, err := os.MkdirTemp("", "parent")
require.NoError(t, err)
defer os.RemoveAll(tmpDir1)
tmpDir2 := filepath.Join(tmpDir1, "child")
err = os.MkdirAll(tmpDir2, 0700)
require.NoError(t, err)
conflict, err := CheckDirectoryConflict(tmpDir2, tmpDir1)
require.NoError(t, err)
assert.True(t, conflict)
})
t.Run("second is subdirectory of first", func(t *testing.T) {
tmpDir1, err := os.MkdirTemp("", "parent")
require.NoError(t, err)
defer os.RemoveAll(tmpDir1)
tmpDir2 := filepath.Join(tmpDir1, "child")
err = os.MkdirAll(tmpDir2, 0700)
require.NoError(t, err)
conflict, err := CheckDirectoryConflict(tmpDir1, tmpDir2)
require.NoError(t, err)
assert.True(t, conflict)
})
t.Run("deeply nested subdirectory", func(t *testing.T) {
tmpDir1, err := os.MkdirTemp("", "parent")
require.NoError(t, err)
defer os.RemoveAll(tmpDir1)
tmpDir2 := filepath.Join(tmpDir1, "a", "b", "c")
err = os.MkdirAll(tmpDir2, 0700)
require.NoError(t, err)
conflict, err := CheckDirectoryConflict(tmpDir1, tmpDir2)
require.NoError(t, err)
assert.True(t, conflict)
conflict, err = CheckDirectoryConflict(tmpDir2, tmpDir1)
require.NoError(t, err)
assert.True(t, conflict)
})
t.Run("symlinked directory", func(t *testing.T) {
tmpDir1, err := os.MkdirTemp("", "real")
require.NoError(t, err)
defer os.RemoveAll(tmpDir1)
tmpDir2, err := os.MkdirTemp("", "symlinks")
require.NoError(t, err)
defer os.RemoveAll(tmpDir2)
symlink := filepath.Join(tmpDir2, "link")
err = os.Symlink(tmpDir1, symlink)
require.NoError(t, err)
conflict, err := CheckDirectoryConflict(tmpDir1, symlink)
require.NoError(t, err)
assert.True(t, conflict)
})
t.Run("relative paths", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "parent")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
subDir := filepath.Join(tmpDir, "subdir")
err = os.MkdirAll(subDir, 0700)
require.NoError(t, err)
err = os.Chdir(tmpDir)
require.NoError(t, err)
conflict, err := CheckDirectoryConflict(".", ".")
require.NoError(t, err)
assert.True(t, conflict)
conflict, err = CheckDirectoryConflict(".", "./subdir")
require.NoError(t, err)
assert.True(t, conflict)
conflict, err = CheckDirectoryConflict("./subdir", ".")
require.NoError(t, err)
assert.True(t, conflict)
})
t.Run("similar prefix but not subdirectory", func(t *testing.T) {
tmpDir1, err := os.MkdirTemp("", "dir")
require.NoError(t, err)
defer os.RemoveAll(tmpDir1)
tmpDir2, err := os.MkdirTemp("", "dir")
require.NoError(t, err)
defer os.RemoveAll(tmpDir2)
conflict, err := CheckDirectoryConflict(tmpDir1, tmpDir2)
require.NoError(t, err)
assert.False(t, conflict)
})
t.Run("non-existent directories are handled", func(t *testing.T) {
// Both directories non-existent but separate - no error, no conflict
conflict, err := CheckDirectoryConflict("/nonexistent/path1", "/nonexistent/path2")
require.NoError(t, err)
assert.False(t, conflict)
// Both directories non-existent, one nested in the other
conflict, err = CheckDirectoryConflict("/nonexistent/parent", "/nonexistent/parent/child")
require.NoError(t, err)
assert.True(t, conflict)
// Same non-existent directory
conflict, err = CheckDirectoryConflict("/nonexistent/same", "/nonexistent/same")
require.NoError(t, err)
assert.True(t, conflict)
})
t.Run("root directory", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "dir")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Root directory handled as a conflict with a subdirectory of the root
conflict, err := CheckDirectoryConflict("/", tmpDir)
require.NoError(t, err)
assert.True(t, conflict)
})
t.Run("overlap but not path prefix", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "dir")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
subDir1 := filepath.Join(tmpDir, "prefix")
subDir2 := filepath.Join(tmpDir, "prefix-but-different-folder")
err = os.Mkdir(subDir1, 0700)
require.NoError(t, err)
err = os.Mkdir(subDir2, 0700)
require.NoError(t, err)
// Two paths, one a string prefix but not an actual path prefix
conflict, err := CheckDirectoryConflict(subDir1, subDir2)
require.NoError(t, err)
assert.False(t, conflict)
})
}