mirror of
https://github.com/filebrowser/filebrowser.git
synced 2026-07-17 16:36:49 +00:00
119 lines
3.4 KiB
Go
119 lines
3.4 KiB
Go
package fbhttp
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/asdine/storm/v3"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/spf13/afero"
|
|
|
|
"github.com/filebrowser/filebrowser/v2/files"
|
|
"github.com/filebrowser/filebrowser/v2/settings"
|
|
"github.com/filebrowser/filebrowser/v2/storage/bolt"
|
|
"github.com/filebrowser/filebrowser/v2/users"
|
|
)
|
|
|
|
// Reproduces the TUS write vector of GHSA-v9g6-9pp4-3w22: a scoped user must
|
|
// not be able to create or write files through a symlinked directory that
|
|
// escapes their scope.
|
|
func TestTusHandlersRejectSymlinkScopeEscape(t *testing.T) {
|
|
root := t.TempDir()
|
|
userScope := filepath.Join(root, "user")
|
|
outside := filepath.Join(root, "otheruser")
|
|
for _, d := range []string{userScope, outside} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// A directory symlink inside the user's scope pointing outside it.
|
|
if err := os.Symlink(outside, filepath.Join(userScope, "escape_link")); err != nil {
|
|
t.Skipf("cannot create symlink: %v", err)
|
|
}
|
|
|
|
key := []byte("test-signing-key")
|
|
|
|
db, err := storm.Open(filepath.Join(t.TempDir(), "db"))
|
|
if err != nil {
|
|
t.Fatalf("failed to open db: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
st, err := bolt.NewStorage(db)
|
|
if err != nil {
|
|
t.Fatalf("failed to get storage: %v", err)
|
|
}
|
|
if err := st.Users.Save(&users.User{
|
|
Username: "u",
|
|
Password: "pw",
|
|
Perm: users.Permissions{Create: true, Modify: true},
|
|
}); err != nil {
|
|
t.Fatalf("failed to save user: %v", err)
|
|
}
|
|
if err := st.Settings.Save(&settings.Settings{Key: key}); err != nil {
|
|
t.Fatalf("failed to save settings: %v", err)
|
|
}
|
|
st.Users = &customFSUser{
|
|
Store: st.Users,
|
|
fs: files.NewScopedFs(afero.NewOsFs(), userScope),
|
|
}
|
|
|
|
// Forge a valid auth token for user ID 1.
|
|
claims := &authToken{
|
|
User: userInfo{ID: 1, Username: "u", Perm: users.Permissions{Create: true, Modify: true}},
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
IssuedAt: jwt.NewNumericDate(time.Now().Add(-time.Minute)),
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
|
},
|
|
}
|
|
signed, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(key)
|
|
if err != nil {
|
|
t.Fatalf("failed to sign token: %v", err)
|
|
}
|
|
|
|
cases := map[string]struct {
|
|
method string
|
|
handler handleFunc
|
|
headers map[string]string
|
|
}{
|
|
"POST create through symlinked dir": {
|
|
method: http.MethodPost,
|
|
handler: tusPostHandler(newMemoryUploadCache()),
|
|
headers: map[string]string{"Upload-Length": "20"},
|
|
},
|
|
"PATCH write through symlinked dir": {
|
|
method: http.MethodPatch,
|
|
handler: tusPatchHandler(newMemoryUploadCache()),
|
|
headers: map[string]string{"Content-Type": "application/offset+octet-stream", "Upload-Offset": "0"},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
tc := tc
|
|
t.Run(name, func(t *testing.T) {
|
|
req, err := http.NewRequest(tc.method, "escape_link/injected.txt", http.NoBody)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.Header.Set("X-Auth", signed)
|
|
for k, v := range tc.headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
|
|
recorder := httptest.NewRecorder()
|
|
handler := handle(tc.handler, "", st, &settings.Server{})
|
|
handler.ServeHTTP(recorder, req)
|
|
|
|
if recorder.Code != http.StatusForbidden {
|
|
t.Errorf("expected 403, got %d", recorder.Code)
|
|
}
|
|
if _, statErr := os.Stat(filepath.Join(outside, "injected.txt")); statErr == nil {
|
|
t.Errorf("VULNERABLE: file was created outside the user's scope")
|
|
}
|
|
})
|
|
}
|
|
}
|