Skip to content

Commit abb8833

Browse files
committed
refactor: standardize IP parsing, improve path traversal security, and enhance mobile file listing and sorting logic
1 parent d420979 commit abb8833

4 files changed

Lines changed: 161 additions & 55 deletions

File tree

desktop/bridge/lansync_mobile.go

Lines changed: 123 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/http"
1010
"os"
1111
"path/filepath"
12+
"sort"
1213
"strings"
1314
"sync"
1415
"time"
@@ -84,7 +85,6 @@ func (cr *ctxReader) Read(p []byte) (int, error) {
8485
}
8586
}
8687

87-
// ── Added OnDevicesDiscovered to Interface ──
8888
type BridgeCallback interface {
8989
OnDeviceDropped(ip string)
9090
OnClipboardDataReceived(data []byte, contentType string)
@@ -193,9 +193,11 @@ func DisconnectDevice(ip string) {
193193
}
194194

195195
func GetSessionToken(ip string) string { return sessionManager.GetOutboundToken(ip) }
196+
196197
func ShareMobileClipboard(ip string, port string, data []byte, contentType string) error {
197198
return clipboardManager.ShareMobileData(ip, port, data, contentType)
198199
}
200+
199201
func GetRemoteFilesJson(ip string, port string, path string) (string, error) {
200202
result, err := androidClient.GetRemoteFiles(ip, port, path)
201203
if err != nil {
@@ -204,10 +206,25 @@ func GetRemoteFilesJson(ip string, port string, path string) (string, error) {
204206
jsonBytes, err := json.Marshal(result)
205207
return string(jsonBytes), err
206208
}
209+
207210
func MakeDirectory(ip string, port string, dir string, name string) error {
208211
return androidClient.MakeDirectory(ip, port, dir, name)
209212
}
210213

214+
func resolveMobilePath(reqPath string) (string, string, error) {
215+
if reqPath == "" {
216+
reqPath = "/"
217+
}
218+
cleanVirtual := filepath.Clean("/" + reqPath)
219+
baseDir := getExposedDir()
220+
absPhysical := filepath.Join(baseDir, cleanVirtual)
221+
222+
if !strings.HasPrefix(absPhysical, baseDir) {
223+
return "", "", fmt.Errorf("path traversal denied")
224+
}
225+
return absPhysical, cleanVirtual, nil
226+
}
227+
211228
func StartMobileServer() {
212229
go func() {
213230
mux := http.NewServeMux()
@@ -304,8 +321,16 @@ func StartMobileServer() {
304321
mux.HandleFunc("/api/ping", authMiddleware(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }))
305322

306323
mux.HandleFunc("/api/disconnect", authMiddleware(func(w http.ResponseWriter, r *http.Request) {
307-
clientIP, _, _ := net.SplitHostPort(r.RemoteAddr)
324+
clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
325+
if err != nil {
326+
clientIP = r.RemoteAddr
327+
}
308328
clientIP = strings.TrimPrefix(clientIP, "::ffff:")
329+
if clientIP == "::1" {
330+
clientIP = "127.0.0.1"
331+
}
332+
333+
cancelTransfersForIP(clientIP)
309334
sessionManager.RemoveSession(clientIP)
310335
if cbProxy != nil && cbProxy.cb != nil {
311336
cbProxy.cb.OnDeviceDropped(clientIP)
@@ -315,60 +340,109 @@ func StartMobileServer() {
315340

316341
mux.HandleFunc("/api/files/list", authMiddleware(func(w http.ResponseWriter, r *http.Request) {
317342
w.Header().Set("Content-Type", "application/json")
318-
requestedPath := r.URL.Query().Get("path")
319-
if requestedPath == "/" {
320-
requestedPath = ""
321-
}
322-
if strings.Contains(requestedPath, "..") {
343+
344+
absPhysical, cleanVirtual, err := resolveMobilePath(r.URL.Query().Get("path"))
345+
if err != nil {
346+
http.Error(w, err.Error(), http.StatusBadRequest)
323347
return
324348
}
325349

326-
fullPath := filepath.Join(getExposedDir(), requestedPath)
327-
entries, err := os.ReadDir(fullPath)
350+
entries, err := os.ReadDir(absPhysical)
328351
if err != nil {
329-
json.NewEncoder(w).Encode(map[string]interface{}{"path": requestedPath, "parent": filepath.Dir(requestedPath), "files": []models.FileInfo{}})
352+
json.NewEncoder(w).Encode(map[string]interface{}{
353+
"path": cleanVirtual,
354+
"parent": filepath.Dir(cleanVirtual),
355+
"files": []models.FileInfo{},
356+
})
330357
return
331358
}
332359

333360
var files []models.FileInfo
334-
for _, e := range entries {
335-
info, err := e.Info()
336-
if err != nil {
361+
for _, entry := range entries {
362+
// Hide Android system dotfiles
363+
if strings.HasPrefix(entry.Name(), ".") {
337364
continue
338365
}
339-
relPath := e.Name()
340-
if requestedPath != "" {
341-
relPath = requestedPath + "/" + e.Name()
366+
367+
info, err := entry.Info()
368+
if err != nil {
369+
continue
342370
}
343-
files = append(files, models.FileInfo{Name: e.Name(), Path: relPath, Size: info.Size(), IsDir: e.IsDir(), ModTime: info.ModTime().Format("2006-01-02 15:04")})
371+
372+
relPath := filepath.ToSlash(filepath.Join(cleanVirtual, entry.Name()))
373+
374+
files = append(files, models.FileInfo{
375+
Name: entry.Name(),
376+
Path: relPath,
377+
Size: info.Size(),
378+
IsDir: entry.IsDir(),
379+
ModTime: info.ModTime().Format("2006-01-02 15:04"),
380+
})
344381
}
345-
parent := filepath.Dir(requestedPath)
346-
if requestedPath == "" {
382+
383+
// Sort correctly (Folders first, then A-Z)
384+
sort.Slice(files, func(i, j int) bool {
385+
if files[i].IsDir != files[j].IsDir {
386+
return files[i].IsDir
387+
}
388+
return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name)
389+
})
390+
391+
parent := filepath.Dir(cleanVirtual)
392+
if cleanVirtual == "/" {
347393
parent = ""
348394
}
349395
if files == nil {
350396
files = []models.FileInfo{}
351397
}
352-
json.NewEncoder(w).Encode(map[string]interface{}{"path": requestedPath, "parent": parent, "files": files})
398+
399+
json.NewEncoder(w).Encode(map[string]interface{}{
400+
"path": cleanVirtual,
401+
"parent": parent,
402+
"files": files,
403+
})
353404
}))
354405

355406
mux.HandleFunc("/api/files/download", authMiddleware(func(w http.ResponseWriter, r *http.Request) {
356-
requestedPath := r.URL.Query().Get("path")
357-
if strings.Contains(requestedPath, "..") {
407+
absPhysical, _, err := resolveMobilePath(r.URL.Query().Get("path"))
408+
if err != nil {
409+
http.Error(w, err.Error(), http.StatusBadRequest)
358410
return
359411
}
360-
http.ServeFile(w, r, filepath.Join(getExposedDir(), requestedPath))
412+
413+
info, err := os.Stat(absPhysical)
414+
if err != nil || info.IsDir() {
415+
http.Error(w, "File not found", http.StatusNotFound)
416+
return
417+
}
418+
419+
f, err := os.Open(absPhysical)
420+
if err != nil {
421+
http.Error(w, "Cannot read file", http.StatusInternalServerError)
422+
return
423+
}
424+
defer f.Close()
425+
426+
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filepath.Base(absPhysical)))
427+
w.Header().Set("Content-Type", "application/octet-stream")
428+
http.ServeContent(w, r, filepath.Base(absPhysical), info.ModTime(), f)
361429
}))
362430

363431
mux.HandleFunc("/api/files/upload", authMiddleware(func(w http.ResponseWriter, r *http.Request) {
364-
dir := r.URL.Query().Get("dir")
365-
if strings.Contains(dir, "..") {
366-
http.Error(w, "Invalid directory", http.StatusBadRequest)
432+
absPhysical, _, err := resolveMobilePath(r.URL.Query().Get("dir"))
433+
if err != nil {
434+
http.Error(w, err.Error(), http.StatusBadRequest)
367435
return
368436
}
369437

370-
clientIP, _, _ := net.SplitHostPort(r.RemoteAddr)
438+
clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
439+
if err != nil {
440+
clientIP = r.RemoteAddr
441+
}
371442
clientIP = strings.TrimPrefix(clientIP, "::ffff:")
443+
if clientIP == "::1" {
444+
clientIP = "127.0.0.1"
445+
}
372446

373447
transferCtx, cancelTransfer := context.WithCancel(r.Context())
374448
registerTransfer(clientIP, cancelTransfer)
@@ -380,8 +454,7 @@ func StartMobileServer() {
380454
return
381455
}
382456

383-
destDir := filepath.Join(getExposedDir(), dir)
384-
os.MkdirAll(destDir, 0755)
457+
os.MkdirAll(absPhysical, 0755)
385458

386459
for {
387460
part, err := reader.NextPart()
@@ -393,12 +466,12 @@ func StartMobileServer() {
393466
}
394467

395468
if part.FormName() == "files" {
396-
filename := part.FileName()
397-
if filename == "" {
469+
filename := filepath.Base(part.FileName()) // Strip malicious traversal attempts
470+
if filename == "" || filename == "." || filename == "/" {
398471
continue
399472
}
400473

401-
dst, err := os.Create(filepath.Join(destDir, filename))
474+
dst, err := os.Create(filepath.Join(absPhysical, filename))
402475
if err == nil {
403476
_, copyErr := io.Copy(dst, &ctxReader{r: part, ctx: transferCtx})
404477
dst.Close()
@@ -413,12 +486,19 @@ func StartMobileServer() {
413486
}))
414487

415488
mux.HandleFunc("/api/files/mkdir", authMiddleware(func(w http.ResponseWriter, r *http.Request) {
416-
dir := r.URL.Query().Get("dir")
489+
absPhysical, _, err := resolveMobilePath(r.URL.Query().Get("dir"))
490+
if err != nil {
491+
http.Error(w, err.Error(), http.StatusBadRequest)
492+
return
493+
}
494+
417495
name := r.URL.Query().Get("name")
418-
if strings.Contains(dir, "..") || strings.Contains(name, "..") {
496+
if name == "" || strings.ContainsAny(name, "/\\") || strings.Contains(name, "..") {
497+
http.Error(w, "Invalid directory name", http.StatusBadRequest)
419498
return
420499
}
421-
os.MkdirAll(filepath.Join(getExposedDir(), dir, name), 0755)
500+
501+
os.MkdirAll(filepath.Join(absPhysical, name), 0755)
422502
w.WriteHeader(http.StatusOK)
423503
}))
424504

@@ -427,8 +507,15 @@ func StartMobileServer() {
427507
}))
428508

429509
mux.HandleFunc("/api/files/cancel", authMiddleware(func(w http.ResponseWriter, r *http.Request) {
430-
clientIP, _, _ := net.SplitHostPort(r.RemoteAddr)
510+
clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
511+
if err != nil {
512+
clientIP = r.RemoteAddr
513+
}
431514
clientIP = strings.TrimPrefix(clientIP, "::ffff:")
515+
if clientIP == "::1" {
516+
clientIP = "127.0.0.1"
517+
}
518+
432519
cancelTransfersForIP(clientIP)
433520
w.WriteHeader(http.StatusOK)
434521
}))

desktop/frontend/src/hooks/useFileTransfer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useRef, useState } from "react";
1+
import { useCallback, useRef, useState, type RefObject } from "react";
22
import {
33
DownloadFile,
44
DownloadFolder,
@@ -23,9 +23,9 @@ type ShowToast = (
2323

2424
export function useFileTransfer(
2525
devices: Device[],
26-
activeDeviceIPRef: React.MutableRefObject<string | null>,
27-
currentPathRef: React.MutableRefObject<string>,
28-
osRef: React.MutableRefObject<string>,
26+
activeDeviceIPRef: RefObject<string | null>,
27+
currentPathRef: RefObject<string>,
28+
osRef: RefObject<string>,
2929
showToast: ShowToast,
3030
) {
3131
const [currentPath, setCurrentPath] = useState("/");

desktop/internal/client/android_client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ func (c *AndroidClient) GetRemoteFiles(targetIP string, targetPort string, targe
175175
return nil, fmt.Errorf("unauthorized")
176176
}
177177

178-
var result map[string]interface{}
178+
var result map[string]any
179179
json.NewDecoder(resp.Body).Decode(&result)
180180
return result, nil
181181
}

0 commit comments

Comments
 (0)