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
111 changes: 111 additions & 0 deletions internal/dao/agent_session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package dao

import (
"ragflow/internal/entity"
)

// AgentSessionDAO data access for agent sessions (conversation table, dialog_id = canvas id).
type AgentSessionDAO struct{}

// NewAgentSessionDAO creates an AgentSessionDAO.
func NewAgentSessionDAO() *AgentSessionDAO {
return &AgentSessionDAO{}
}

// GetByID returns a single agent session by conversation ID.
func (d *AgentSessionDAO) GetByID(id string) (*entity.AgentSession, error) {
var s entity.AgentSession
err := DB.Where("id = ?", id).First(&s).Error
return &s, err
}

// ListByAgentID returns all sessions for a given agent (canvas) ID, ordered newest first.
func (d *AgentSessionDAO) ListByAgentID(agentID string) ([]*entity.AgentSession, error) {
var sessions []*entity.AgentSession
err := DB.Where("dialog_id = ?", agentID).
Order("create_time DESC").
Find(&sessions).Error
return sessions, err
}

// ListByAgentIDPaged returns paginated sessions for a given agent ID.
func (d *AgentSessionDAO) ListByAgentIDPaged(
agentID string,
page, pageSize int,
orderby string,
desc bool,
sessionID, userID, keywords string,
) ([]*entity.AgentSession, int64, error) {
base := DB.Model(&entity.AgentSession{}).Where("dialog_id = ?", agentID)

if sessionID != "" {
base = base.Where("id = ?", sessionID)
}
if userID != "" {
base = base.Where("user_id = ?", userID)
}
if keywords != "" {
base = base.Where("name LIKE ?", "%"+keywords+"%")
}

var total int64
if err := base.Count(&total).Error; err != nil {
return nil, 0, err
}

order := orderby
if order == "" {
order = "update_time"
}
if desc {
order += " DESC"
} else {
order += " ASC"
}

query := base.Order(order)
if page > 0 && pageSize > 0 {
Comment on lines +72 to +83
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In GORM v1.25.x, does db.Order(string) treat input as raw SQL, requiring allowlisting/safe column construction for user-provided sort fields?

💡 Result:

Yes, in GORM v1.25.x, the db.Order(string) method treats its input as raw SQL and is not automatically sanitized against SQL injection [1][2]. Because SQL standards do not allow column names or directions to be parameterized in an ORDER BY clause, GORM does not provide built-in escaping for this method [3][4]. To securely handle user-provided sort fields, you must use an allowlist approach [1][4]. Never pass raw user input directly into db.Order [4]. Instead, map the user input to a predefined set of safe, hardcoded column names [4]. Example of secure pattern: // SECURE: Use a map to validate allowed columns allowedOrders:= map[string]string{ "id": "id", "name": "name", "price": "price", } if col, ok:= allowedOrders[userInput]; ok { db.Order(col + " asc").Find(&products) } else { // Default fallback db.Order("id asc").Find(&products) } The official GORM security documentation explicitly lists db.Order among the methods that are not escaped and requires developer caution to avoid SQL injection [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section with line numbers
sed -n '50,120p' internal/dao/agent_session.go

# Find where orderby/desc are defined/received in this file
rg -n "orderby|desc\b|Order\\(" internal/dao/agent_session.go

# Show DAO method signature around the snippet (best effort)
rg -n "ListByAgentIDPaged|Paged|ByAgent" internal/dao/agent_session.go

Repository: infiniflow/ragflow

Length of output: 2190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Where ListByAgentIDPaged is called
rg -n "ListByAgentIDPaged\\(" -S internal | head -n 50

# Trace how orderby/desc are populated (look for "orderby" string or query params)
rg -n "\\borderby\\b" -S internal | head -n 80
rg -n "\\bdesc\\b" -S internal/handler internal/service 2>/dev/null | head -n 80

Repository: infiniflow/ragflow

Length of output: 16829


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' internal/handler/agent.go
sed -n '200,320p' internal/service/agent.go

# Narrow search for any allowlist/sanitization of orderby in agent flow
rg -n "orderby.*(allowed|allow|whitelist|map|switch|case)" internal/handler/agent.go internal/service/agent.go internal/dao/agent_session.go || true

Repository: infiniflow/ragflow

Length of output: 9598


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect internal/dao/agent_session.go imports + the full ListByAgentIDPaged function for exact context
sed -n '1,120p' internal/dao/agent_session.go
sed -n '120,220p' internal/dao/agent_session.go

# 2) Inspect the handler code around where orderby/desc are set for agent session listing
sed -n '330,470p' internal/handler/agent.go

# 3) Check entity fields for AgentSession to ensure allowlist column names match actual struct/columns
fd -a -e go internal/entity | head -n 50
rg -n "type AgentSession struct" internal/entity || true
ls internal/entity | rg -i "agent|session" || true
fd -a "agent_session.go" internal/entity || true
fd -a "session.go" internal/entity || true

Repository: infiniflow/ragflow

Length of output: 8416


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' internal/service/agent.go
sed -n '220,320p' internal/service/agent.go
rg -n "ListAgentSessions\\(" internal/service/agent.go
rg -n "Orderby" internal/service/agent.go

Repository: infiniflow/ragflow

Length of output: 9737


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check GORM version from go.mod
cat go.mod | rg -n "gorm\\.io/gorm" || true
cat go.mod | rg -n "gorm\\.io/datatypes|gorm\\.io/driver" || true

# See if clause.OrderByColumn is used anywhere in repo
rg -n "gorm\\.io/gorm/clause|OrderByColumn" internal | head -n 50

Repository: infiniflow/ragflow

Length of output: 119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the AgentSession entity definition
rg -n "type AgentSession struct" internal/entity -S || true
fd --full-path "*session*.go" internal/entity | head -n 50
fd --full-path "*agent*session*.go" internal/entity | head -n 50

Repository: infiniflow/ragflow

Length of output: 428


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' internal/entity/canvas.go
rg -n "type AgentSession struct" -n internal/entity/canvas.go
sed -n '1,80p' internal/entity/canvas.go | nl -ba | sed -n '1,120p'

Repository: infiniflow/ragflow

Length of output: 4439


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "type BaseModel struct" internal/entity -S
rg -n "create_time|update_time" internal/entity -S
sed -n '1,200p' internal/entity/base_model.go 2>/dev/null || true
fd --full-path "base*model*.go" internal/entity | head -n 20

Repository: infiniflow/ragflow

Length of output: 3139


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,160p' internal/entity/base.go
rg -n "gorm\\.io/gorm/clause" -S . || true
rg -n "OrderByColumn" -S . || true

Repository: infiniflow/ragflow

Length of output: 4482


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Best-effort: look into module cache for clause.OrderByColumn definition
if command -v go >/dev/null 2>&1; then
  GOPATH="$(go env GOPATH 2>/dev/null || true)"
  if [ -n "${GOPATH:-}" ] && [ -d "$GOPATH/pkg/mod" ]; then
    rg -n "type OrderByColumn" "$GOPATH/pkg/mod" 2>/dev/null | head -n 20 || true
    rg -n "OrderByColumn" "$GOPATH/pkg/mod/gorm.io/gorm@"*/clause/* 2>/dev/null | head -n 50 || true
  else
    echo "GOPATH/pkg/mod not found"
  fi
else
  echo "go not available"
fi

# Confirm clause import usage in repo (should be none)
rg -n "gorm\\.io/gorm/clause" -S . || true

Repository: infiniflow/ragflow

Length of output: 87


Prevent SQL injection from user-controlled orderby in AgentSessionDAO.ListByAgentIDPaged.

internal/dao/agent_session.go builds order := orderby (only defaulting when empty) and then calls query := base.Order(order), so the orderby query param reaches GORM as raw ORDER BY SQL (not identifier-escaped) via internal/handler/agent.go → service → DAO.

Fix: allowlist orderby to a fixed set of permitted column names (e.g., id, name, create_time, update_time, etc.) and construct ORDER BY <mapped_column> <ASC|DESC> using only the mapped value and the desc boolean.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/dao/agent_session.go` around lines 72 - 83,
AgentSessionDAO.ListByAgentIDPaged currently assigns user-controlled orderby
directly to order and passes it into base.Order(order), enabling SQL injection;
replace that by validating orderby against a hardcoded allowlist (e.g.,
map[string]string
{"id":"id","name":"name","create_time":"create_time","update_time":"update_time"})
and mapping the input to the canonical column name, then build order as
mappedColumn + (desc ? " DESC" : " ASC") and pass only that into base.Order;
ensure the code references the existing orderby variable, the desc boolean, and
the call site base.Order(order) so no raw user input flows into the ORDER BY
clause.

query = query.Offset((page - 1) * pageSize).Limit(pageSize)
}

var sessions []*entity.AgentSession
err := query.Find(&sessions).Error
return sessions, total, err
}

// DeleteByID hard-deletes a session by ID.
func (d *AgentSessionDAO) DeleteByID(id string) error {
return DB.Unscoped().Where("id = ?", id).Delete(&entity.AgentSession{}).Error
}

// GetByIDAndAgentID returns a session only if it belongs to the given agent.
func (d *AgentSessionDAO) GetByIDAndAgentID(sessionID, agentID string) (*entity.AgentSession, error) {
var s entity.AgentSession
err := DB.Where("id = ? AND dialog_id = ?", sessionID, agentID).First(&s).Error
return &s, err
}

// BelongsToAgent checks whether a session belongs to a specific agent (canvas owner).
func (d *AgentSessionDAO) BelongsToAgent(sessionID, agentID string) (bool, error) {
var count int64
err := DB.Model(&entity.AgentSession{}).
Where("id = ? AND dialog_id = ?", sessionID, agentID).
Count(&count).Error
return count > 0, err
}
22 changes: 22 additions & 0 deletions internal/entity/canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,28 @@

package entity

import "encoding/json"

// AgentSession maps to the conversation table for agent-owned sessions.
// It extends the basic ChatSession fields with agent-specific columns.
type AgentSession struct {
ID string `gorm:"column:id;primaryKey;size:32" json:"id"`
DialogID string `gorm:"column:dialog_id;size:32;not null;index" json:"agent_id"`
Name *string `gorm:"column:name;size:255" json:"name,omitempty"`
Message json.RawMessage `gorm:"column:message;type:longtext" json:"message,omitempty"`
Reference json.RawMessage `gorm:"column:reference;type:longtext" json:"reference"`
UserID *string `gorm:"column:user_id;size:255" json:"user_id,omitempty"`
ExpUserID *string `gorm:"column:exp_user_id;size:255" json:"exp_user_id,omitempty"`
Source *string `gorm:"column:source;size:64" json:"source,omitempty"`
VersionTitle *string `gorm:"column:version_title;size:255" json:"version_title,omitempty"`
BaseModel
}

// TableName maps AgentSession to the conversation table.
func (AgentSession) TableName() string {
return "conversation"
}

// UserCanvas user canvas model
type UserCanvas struct {
ID string `gorm:"column:id;primaryKey;size:32" json:"id"`
Expand Down
Loading