Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions dice/ext_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -955,14 +955,26 @@ func LogEditByID(ctx *MsgContext, groupID, logName, content string, messageID in
}

func GetLogTxt(ctx *MsgContext, groupID string, logName string, fileNamePrefix string) (string, error) {
// 创建临时文件
tempLog, err := os.CreateTemp("", fmt.Sprintf(
// 创建临时文件,优先使用海豹数据目录
dice := ctx.Dice
tempDir := filepath.Join(dice.BaseConfig.DataDir, "temp")
_ = os.MkdirAll(tempDir, 0o755)
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent error suppression: The error from os.MkdirAll is being ignored with the blank identifier. If directory creation fails (e.g., due to permission issues), the subsequent os.CreateTemp call will also fail but with a less informative error message. Consider checking the MkdirAll error and returning a specific error message like "创建临时目录失败: %w" to help users diagnose permission or disk space issues.

Suggested change
_ = os.MkdirAll(tempDir, 0o755)
if err := os.MkdirAll(tempDir, 0o755); err != nil {
return "", fmt.Errorf("创建临时目录失败: %w", err)
}

Copilot uses AI. Check for mistakes.

tempLog, err := os.CreateTemp(tempDir, fmt.Sprintf(
"%s(*).txt",
utils.FilenameClean(fileNamePrefix),
))
if err != nil {
return "", errors.New("log导出出现未知错误")
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error handling: When os.CreateTemp fails at line 967, a generic error message "log导出出现未知错误" is returned without the underlying error details. However, other error paths in this function properly wrap errors with their context (lines 1019, 1040, 1051). For consistency and better debugging, wrap the error: return "", fmt.Errorf("创建临时文件失败: %w", err).

Suggested change
return "", errors.New("log导出出现未知错误")
return "", fmt.Errorf("创建临时文件失败: %w", err)

Copilot uses AI. Check for mistakes.
}

// 设置文件权限为644,确保NapCat可以读取
if err1 := os.Chmod(tempLog.Name(), 0o644); err1 != nil {
_ = tempLog.Close()
_ = os.Remove(tempLog.Name())
return "", errors.New("设置临时文件权限失败")
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error handling: When os.Chmod fails, the error message is generic "设置临时文件权限失败" without preserving the underlying error context. However, other error paths in this function (lines 1040, 1051) properly wrap errors with fmt.Errorf and %w. For consistency and better debugging, wrap the error: return "", fmt.Errorf("设置临时文件权限失败: %w", err1).

Suggested change
return "", errors.New("设置临时文件权限失败")
return "", fmt.Errorf("设置临时文件权限失败: %w", err1)

Copilot uses AI. Check for mistakes.
}
Comment on lines +971 to +976
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially excessive file permissions: Setting file permissions to 0o644 (readable by all users) may be more permissive than necessary. Temporary log files could contain sensitive information like user IDs, nicknames, and message content. Consider using 0o600 (readable only by owner) unless group/other read access is specifically required for the NapCat integration. If broader permissions are necessary, document the security rationale in a comment.

Copilot uses AI. Check for mistakes.

defer func() {
_ = tempLog.Close()
if err != nil {
Expand All @@ -989,9 +1001,9 @@ func GetLogTxt(ctx *MsgContext, groupID string, logName string, fileNamePrefix s
return
default:
// 获取当前游标对应的数据
cursorLines, cursor, err := service.LogGetCursorLines(ctx.Dice.DBOperator, groupID, logName, currentCursor)
if err != nil {
resultCh <- err
cursorLines, cursor, err1 := service.LogGetCursorLines(ctx.Dice.DBOperator, groupID, logName, currentCursor)
if err1 != nil {
resultCh <- err1
return
}

Expand All @@ -1003,8 +1015,8 @@ func GetLogTxt(ctx *MsgContext, groupID string, logName string, fileNamePrefix s
counter++
}
// ========== 新增:每批写入后强制同步 ==========
if err := tempLog.Sync(); err != nil { // 确保批次数据落盘
resultCh <- fmt.Errorf("批次同步失败: %w", err)
if err1 := tempLog.Sync(); err1 != nil { // 确保批次数据落盘
resultCh <- fmt.Errorf("批次同步失败: %w", err1)
}

// 如果没有下一页,则成功完成
Expand All @@ -1020,20 +1032,25 @@ func GetLogTxt(ctx *MsgContext, groupID string, logName string, fileNamePrefix s
}()

// 等待 goroutine 完成或超时
if err := <-resultCh; err != nil {
return "", err
if err1 := <-resultCh; err1 != nil {
return "", err1
}
// 2. 确保文件指针回到开头
if _, err := tempLog.Seek(0, 0); err != nil {
return "", fmt.Errorf("重置文件指针失败: %w", err)
if _, err1 := tempLog.Seek(0, 0); err1 != nil {
return "", fmt.Errorf("重置文件指针失败: %w", err1)
}

// 如果没有任何数据,返回错误
if counter == 0 {
return "", errors.New("此log不存在,或条目数为空,名字是否正确?")
}

return tempLog.Name(), nil
// 确保返回绝对路径
absPath, err := filepath.Abs(tempLog.Name())
if err != nil {
return "", fmt.Errorf("获取文件绝对路径失败: %w", err)
}
return absPath, nil
}

func LogSendToBackend(ctx *MsgContext, groupID string, logName string) (bool, string, error) {
Expand Down
38 changes: 25 additions & 13 deletions dice/storylog/upload_v105.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,25 @@ func GetLogTxtAndParquetFile(env UploadEnv) (*os.File, *bytes.Buffer, error) {
),
))
buf := new(bytes.Buffer)
tempLog, err := os.CreateTemp("", fmt.Sprintf(

// 尝试创建临时文件,优先使用海豹数据目录
tempDir := filepath.Join(env.Dir, "temp")
_ = os.MkdirAll(tempDir, 0o755)
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent error suppression: The error from os.MkdirAll is being ignored with the blank identifier. If directory creation fails (e.g., due to permission issues), the subsequent os.CreateTemp call will also fail but with a less informative error message. Consider checking the MkdirAll error and returning a specific error to help diagnose permission or disk space issues.

Suggested change
_ = os.MkdirAll(tempDir, 0o755)
if err := os.MkdirAll(tempDir, 0o755); err != nil {
return nil, nil, fmt.Errorf("创建临时目录失败: %w", err)
}

Copilot uses AI. Check for mistakes.

tempLog, err := os.CreateTemp(tempDir, fmt.Sprintf(
"%s(*).txt",
utils.FilenameClean("sealdice_v105_prefix_"),
))
if err != nil {
return nil, nil, errors.New("log导出出现未知错误")
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error handling: When os.Chmod fails, the error message is wrapped as "设置临时文件权限失败: %w" preserving the original error context. However, when os.CreateTemp fails, the generic error message "log导出出现未知错误" is returned, losing the original error context. For better debugging, preserve the original error with fmt.Errorf("创建临时文件失败: %w", err).

Suggested change
return nil, nil, errors.New("log导出出现未知错误")
return nil, nil, fmt.Errorf("创建临时文件失败: %w", err)

Copilot uses AI. Check for mistakes.
}

// 设置文件权限为644,确保其他进程可以读取
if err1 := os.Chmod(tempLog.Name(), 0o644); err1 != nil {
Comment on lines +50 to +51
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially excessive file permissions: Setting file permissions to 0o644 (readable by all users) may be more permissive than necessary. Temporary log files could contain sensitive information. Consider using 0o600 (readable only by owner) unless broader permissions are specifically required. If world-readable permissions are necessary for inter-process communication, document the security rationale in a comment.

Suggested change
// 设置文件权限为644,确保其他进程可以读取
if err1 := os.Chmod(tempLog.Name(), 0o644); err1 != nil {
// 设置文件权限为600,仅允许当前用户读写,避免临时日志被其他用户读取
if err1 := os.Chmod(tempLog.Name(), 0o600); err1 != nil {

Copilot uses AI. Check for mistakes.
_ = os.Remove(tempLog.Name())
return nil, nil, fmt.Errorf("设置临时文件权限失败: %w", err1)
}

defer func() {
if err != nil {
_ = os.Remove(tempLog.Name())
Expand All @@ -66,9 +78,9 @@ func GetLogTxtAndParquetFile(env UploadEnv) (*os.File, *bytes.Buffer, error) {
return
default:
// 获取当前游标对应的数据
cursorLines, cursor, err0 := service.LogGetExportCursorLines(env.Db, env.GroupID, env.LogName, currentCursor)
if err0 != nil {
resultCh <- err0
cursorLines, cursor, err1 := service.LogGetExportCursorLines(env.Db, env.GroupID, env.LogName, currentCursor)
if err1 != nil {
resultCh <- err1
return
}

Expand All @@ -80,13 +92,13 @@ func GetLogTxtAndParquetFile(env UploadEnv) (*os.File, *bytes.Buffer, error) {
counter++
}
// ========== 新增:每批写入后强制同步 ==========
if err = tempLog.Sync(); err != nil { // 确保批次数据落盘
resultCh <- fmt.Errorf("批次同步失败: %w", err)
if err1 := tempLog.Sync(); err1 != nil { // 确保批次数据落盘
resultCh <- fmt.Errorf("批次同步失败: %w", err1)
}

_, err0 = parquetBuffer.Write(cursorLines)
if err0 != nil {
resultCh <- err0
_, err2 := parquetBuffer.Write(cursorLines)
if err2 != nil {
resultCh <- err2
return
}

Expand All @@ -103,8 +115,8 @@ func GetLogTxtAndParquetFile(env UploadEnv) (*os.File, *bytes.Buffer, error) {
}()

// 等待 goroutine 完成或超时
if err = <-resultCh; err != nil {
return nil, nil, err
if err1 := <-resultCh; err1 != nil {
return nil, nil, err1
}

// 如果没有任何数据,返回错误
Expand All @@ -115,8 +127,8 @@ func GetLogTxtAndParquetFile(env UploadEnv) (*os.File, *bytes.Buffer, error) {
compressOption := parquet.Compression(&zstd.Codec{})
writer := parquet.NewGenericWriter[model.LogOneItemParquet](buf, compressOption)
// 2. 确保文件指针回到开头
if _, err = tempLog.Seek(0, 0); err != nil {
return nil, nil, fmt.Errorf("重置文件指针失败: %w", err)
if _, err1 := tempLog.Seek(0, 0); err1 != nil {
return nil, nil, fmt.Errorf("重置文件指针失败: %w", err1)
}
// 写入到writer中
_, err = parquet.CopyRows(writer, parquetBuffer.Rows())
Expand Down
124 changes: 119 additions & 5 deletions message/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,26 +307,94 @@ func calculateMD5(header http.Header) string {

// ExtractLocalTempFile 按路径提取临时文件,路径可以是 http/base64/本地路径
func ExtractLocalTempFile(path string) (string, string, error) {
// 如果是 files:// 协议且指向本地文件,直接解析并返回
if strings.HasPrefix(path, "files://") {
filePath := path[8:] // 移除 "files://" 前缀

var absPath string

// 处理 files:///path 格式(三个斜杠开头)
filePath = strings.TrimPrefix(filePath, "/")

// 检查是否为有效的绝对路径

// 检查是否为有效的绝对路径
Comment on lines +320 to +321
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate comment detected. The comment on line 319 "检查是否为有效的绝对路径" is repeated on line 321. Remove the duplicate comment.

Suggested change
// 检查是否为有效的绝对路径

Copilot uses AI. Check for mistakes.
if filepath.IsAbs(filePath) {
// 如果已经是绝对路径,清理并使用
absPath = filepath.Clean(filePath)
} else {
// 如果是相对路径,转换为绝对路径
var err error
absPath, err = filepath.Abs(filePath)
if err != nil {
return "", "", fmt.Errorf("获取文件绝对路径失败: %w", err)
}
}

info, err := os.Stat(absPath)
if err != nil {
return "", "", fmt.Errorf("文件不存在或无法访问: %w", err)
}

// 检查文件权限,如果不是644则修改
if info.Mode().Perm() != 0o644 {
if err := os.Chmod(absPath, 0o644); err != nil {
return "", "", fmt.Errorf("设置文件权限失败: %w", err)
}
}

return info.Name(), absPath, nil
Comment on lines +310 to +346
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path traversal vulnerability: The files:// protocol handler in ExtractLocalTempFile lacks security validation. Unlike the FilepathToFileElement function (lines 445-450), this code does not verify that the resolved absolute path is within allowed directories (cwd, data directory, or temp directory). An attacker could potentially use this to access arbitrary files on the system by crafting a malicious files:// URL with path traversal sequences like "../../../etc/passwd".

Copilot uses AI. Check for mistakes.
}

// 对于其他协议(http/base64),按原逻辑处理
fileElement, err := FilepathToFileElement(path)
if err != nil {
return "", "", err
}
temp, err := os.CreateTemp("", "temp-")
defer func(temp *os.File) {
_ = temp.Close()
}(temp)

// 尝试获取海豹数据目录,如果失败则使用系统临时目录
var tempDir string
if wd, err1 := os.Getwd(); err1 == nil {
tempDir = filepath.Join(wd, "data", "temp")
_ = os.MkdirAll(tempDir, 0o755)
Comment on lines +357 to +359
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent error suppression: The error from os.MkdirAll is being ignored with the blank identifier. If directory creation fails (e.g., due to permission issues), the subsequent os.CreateTemp will fail with a less informative error. While falling back to system temp directory is acceptable, consider logging the error to help diagnose configuration issues in production environments.

Copilot uses AI. Check for mistakes.
} else {
tempDir = ""
}

temp, err := os.CreateTemp(tempDir, "temp-")
if err != nil {
return "", "", err
}

// 设置文件权限为644,确保其他进程可以读取
if err1 := os.Chmod(temp.Name(), 0o644); err1 != nil {
_ = temp.Close()
_ = os.Remove(temp.Name())
return "", "", fmt.Errorf("设置临时文件权限失败: %w", err1)
}
Comment on lines +369 to +374
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially excessive file permissions: Setting file permissions to 0o644 (readable by all users) may be more permissive than necessary for temporary files. Consider using 0o600 (readable only by owner) unless broader permissions are specifically required for inter-process communication. If world-readable permissions are necessary, document the security rationale in a comment.

Copilot uses AI. Check for mistakes.

defer func(temp *os.File) {
_ = temp.Close()
}(temp)

data, err := io.ReadAll(fileElement.Stream)
if err != nil {
_ = os.Remove(temp.Name())
return "", "", err
}

_, err = temp.Write(data)
if err != nil {
_ = os.Remove(temp.Name())
return "", "", err
}
return fileElement.File, temp.Name(), nil

// 确保返回绝对路径
absPath, err := filepath.Abs(temp.Name())
if err != nil {
return "", "", fmt.Errorf("获取文件绝对路径失败: %w", err)
}
return fileElement.File, absPath, nil
}

func FilepathToFileElement(fp string) (*FileElement, error) {
Expand All @@ -353,6 +421,52 @@ func FilepathToFileElement(fp string) (*FileElement, error) {
URL: fp,
}
return r, nil
} else if strings.HasPrefix(fp, "files://") {
// 处理 files:// 协议,直接读取本地文件
filePath := fp[8:] // 移除 "files://" 前缀
if strings.HasPrefix(filePath, "/") && len(filePath) > 1 {
filePath = filePath[1:] // 移除开头的斜杠,files:///path -> /path
}

info, err := os.Stat(filePath)
if err != nil {
return nil, fmt.Errorf("文件不存在或无法访问: %w", err)
}

Comment on lines +427 to +435
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect path parsing logic. The condition if strings.HasPrefix(filePath, "/") && len(filePath) > 1 will strip the leading slash from paths like "/home/user/file.txt", converting it to "home/user/file.txt", which becomes a relative path. This is incorrect for Unix absolute paths. For files:///path/to/file, after removing "files://" we get "/path/to/file", then this code removes another "/" resulting in "path/to/file" which is now a relative path, breaking the subsequent absolute path check.

Suggested change
if strings.HasPrefix(filePath, "/") && len(filePath) > 1 {
filePath = filePath[1:] // 移除开头的斜杠,files:///path -> /path
}
info, err := os.Stat(filePath)
if err != nil {
return nil, fmt.Errorf("文件不存在或无法访问: %w", err)
}
info, err := os.Stat(filePath)
if err != nil {
return nil, fmt.Errorf("文件不存在或无法访问: %w", err)
}

Copilot uses AI. Check for mistakes.
if info.Size() == 0 || info.Size() >= maxFileSize {
return nil, errors.New("invalid file size")
}

afn, err := filepath.Abs(filePath)
if err != nil {
return nil, fmt.Errorf("获取文件绝对路径失败: %w", err)
}

// 允许访问海豹数据目录和系统临时目录
cwd, _ := os.Getwd()
dataDir := filepath.Join(cwd, "data")
if !strings.HasPrefix(afn, cwd) && !strings.HasPrefix(afn, os.TempDir()) && !strings.HasPrefix(afn, dataDir) {
return nil, errors.New("restricted file path")
}
Comment on lines +445 to +450
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Insufficient path traversal protection. The current security check only validates that paths start with cwd, dataDir, or os.TempDir(), but this is vulnerable to symlink attacks and doesn't prevent access to subdirectories outside the intended scope. After calling filepath.Clean() and filepath.Abs(), you should validate the canonical path again. Additionally, the dataDir check is redundant since dataDir is cwd + "/data", which would already match the cwd prefix check.

Copilot uses AI. Check for mistakes.

filesuffix := path.Ext(filePath)
content, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("读取文件失败: %w", err)
}

contenttype := mime.TypeByExtension(filesuffix)
if len(contenttype) == 0 {
contenttype = "application/octet-stream"
}

r := &FileElement{
Stream: bytes.NewReader(content),
ContentType: contenttype,
File: info.Name(),
URL: fp,
}
return r, nil
} else if strings.HasPrefix(fp, "base64://") {
content, err := base64.StdEncoding.DecodeString(fp[9:])
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions utils/dboperator/engine/sqlite/engine_sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"gorm.io/gorm"

"sealdice-core/logger"
"sealdice-core/model"
"sealdice-core/utils/cache"
"sealdice-core/utils/constant"
)
Expand Down Expand Up @@ -251,6 +252,12 @@ func (s *SQLiteEngine) CensorDBInit() error {
censorContext := context.WithValue(s.ctx, cache.CacheKey, cache.CensorsDBCacheKey)
readDB = readDB.WithContext(censorContext)
writeDB = writeDB.WithContext(censorContext)

// 创建 CensorLog 表结构
if err := writeDB.AutoMigrate(&model.CensorLog{}); err != nil {
return err
}

s.readList[CensorsDBKey] = readDB
s.writeList[CensorsDBKey] = writeDB
return nil
Expand Down