Przeglądaj źródła

fix(handler): tighten tg-import against traversal, orphan files, stale ctx

- Harden normalizeRel via path.Clean + IsAbs (rejects bare .., drive letters)
- Clean up on-disk session+origin/ when duplicate phone or DB create fails
- Include pyrogram_err/tdata_err/tmp_dir in 422 response body for diagnostics
- Auto-test uses detached context.Background so a dropped HTTP request
  cannot falsely mark a new account dead

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dot 2 tygodni temu
rodzic
commit
c6253d06ab
1 zmienionych plików z 43 dodań i 6 usunięć
  1. 43 6
      internal/handler/tg_account.go

+ 43 - 6
internal/handler/tg_account.go

@@ -6,6 +6,7 @@ import (
 	"io"
 	"mime/multipart"
 	"os"
+	"path"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -285,14 +286,36 @@ func (h *TgAccountHandler) Import(c *gin.Context) {
 
 	res, err := sessionimport.Import(ctx, h.sessionsDir, staged)
 	if err != nil {
-		Fail(c, 422, err.Error())
+		detail := gin.H{"error": err.Error()}
+		if res != nil {
+			detail["pyrogram_err"] = res.PyrogramErr
+			detail["tdata_err"] = res.TdataErr
+			detail["tmp_dir"] = stageDir
+		}
+		c.JSON(422, gin.H{
+			"code":    422,
+			"message": err.Error(),
+			"data":    detail,
+		})
 		return
 	}
 
+	cleanupOnFail := func() {
+		if res != nil {
+			if res.SessionFile != "" {
+				_ = os.Remove(res.SessionFile)
+			}
+			if res.OriginDir != "" {
+				_ = os.RemoveAll(res.OriginDir)
+			}
+		}
+	}
+
 	// Duplicate check.
 	var count int64
 	h.store.DB.Model(&model.TgAccount{}).Where("phone = ?", res.Meta.Phone).Count(&count)
 	if count > 0 {
+		cleanupOnFail()
 		Fail(c, 409, "该手机号已存在")
 		return
 	}
@@ -302,6 +325,7 @@ func (h *TgAccountHandler) Import(c *gin.Context) {
 	if res.TwoFAPlaintext != "" && h.crypto != nil {
 		twoFAEnc, err = h.crypto.Encrypt(res.TwoFAPlaintext)
 		if err != nil {
+			cleanupOnFail()
 			Fail(c, 500, "encrypt 2fa: "+err.Error())
 			return
 		}
@@ -330,9 +354,11 @@ func (h *TgAccountHandler) Import(c *gin.Context) {
 	}
 	if err := h.store.DB.Create(&acc).Error; err != nil {
 		if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
+			cleanupOnFail()
 			Fail(c, 409, "该手机号已存在")
 			return
 		}
+		cleanupOnFail()
 		Fail(c, 500, "db create: "+err.Error())
 		return
 	}
@@ -340,8 +366,11 @@ func (h *TgAccountHandler) Import(c *gin.Context) {
 	// Rebuild account manager so new account is Acquireable.
 	h.reloadAccounts()
 
-	// Auto-test.
-	testResult := h.runTest(c.Request.Context(), &acc)
+	// Auto-test — detach from the HTTP request context so a client disconnect
+	// cannot falsely mark a freshly imported account as dead.
+	bgCtx, bgCancel := context.WithTimeout(context.Background(), 45*time.Second)
+	defer bgCancel()
+	testResult := h.runTest(bgCtx, &acc)
 	if testResult["status"] == "fail" {
 		h.store.DB.Model(&acc).Updates(map[string]any{
 			"enabled":       false,
@@ -409,11 +438,19 @@ func normalizeRel(filename, topPrefix string) string {
 	if topPrefix != "" && strings.HasPrefix(p, topPrefix+"/") {
 		p = strings.TrimPrefix(p, topPrefix+"/")
 	}
-	// Reject any path that tries to escape.
-	if strings.Contains(p, "../") || strings.HasPrefix(p, "/") {
+	// Use path.Clean (forward-slash aware) to resolve . and .. components,
+	// then reject any result that escapes the staging root.
+	cleaned := path.Clean(p)
+	if cleaned == "." || cleaned == ".." {
+		return ""
+	}
+	if strings.HasPrefix(cleaned, "../") {
+		return ""
+	}
+	if filepath.IsAbs(cleaned) || strings.HasPrefix(cleaned, "/") {
 		return ""
 	}
-	return p
+	return cleaned
 }
 
 func saveUpload(fh *multipart.FileHeader, dst string) error {