diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e974ae98b..53571dd76 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -2,7 +2,7 @@ services: # 只启动依赖的基础设施服务 postgres: - image: paradedb/paradedb:v0.18.9-pg17 + image: paradedb/paradedb:v0.21.4-pg17 container_name: WeKnora-postgres-dev ports: - "${DB_PORT:-5432}:5432" diff --git a/internal/application/service/knowledge.go b/internal/application/service/knowledge.go index e72bc8be0..30e1f9fec 100644 --- a/internal/application/service/knowledge.go +++ b/internal/application/service/knowledge.go @@ -6389,7 +6389,16 @@ func (s *knowledgeService) ProcessDocument(ctx context.Context, t *asynq.Task) e // 处理不同类型的导入:文件、URL、文本段落 var chunks []*proto.Chunk if payload.URL != "" { - // URL导入 + // URL导入 - 再次进行 SSRF 验证(防止 DNS 重绑定攻击) + if safe, reason := secutils.IsSSRFSafeURL(payload.URL); !safe { + logger.Errorf(ctx, "URL rejected for SSRF protection in ProcessDocument: %s, reason: %s", payload.URL, reason) + knowledge.ParseStatus = "failed" + knowledge.ErrorMessage = "URL is not allowed for security reasons" + knowledge.UpdatedAt = time.Now() + s.repo.UpdateKnowledge(ctx, knowledge) + return nil + } + urlResp, err := s.docReaderClient.ReadFromURL(ctx, &proto.ReadFromURLRequest{ Url: payload.URL, Title: knowledge.Title, diff --git a/internal/utils/security.go b/internal/utils/security.go index c8fd85e87..3a200a9c3 100644 --- a/internal/utils/security.go +++ b/internal/utils/security.go @@ -354,15 +354,21 @@ func IsSSRFSafeURL(rawURL string) (bool, string) { // Perform DNS resolution to check the resolved IP // This prevents DNS rebinding attacks where a domain resolves to internal IPs ips, err := net.LookupIP(hostname) - if err == nil { - for _, resolvedIP := range ips { - if restricted, reason := isRestrictedIP(resolvedIP); restricted { - return false, fmt.Sprintf("hostname %s resolves to restricted IP %s: %s", hostname, resolvedIP.String(), reason) - } + if err != nil { + // DNS resolution failed - reject the URL for security + // This prevents attacks where: + // 1. The domain is only resolvable within internal network (intranet domains) + // 2. Different DNS servers between validation and actual request + // 3. Attacker-controlled DNS that selectively responds + return false, fmt.Sprintf("DNS resolution failed for hostname %s: cannot verify if it resolves to safe IP", hostname) + } + + // Check if any resolved IP is restricted + for _, resolvedIP := range ips { + if restricted, reason := isRestrictedIP(resolvedIP); restricted { + return false, fmt.Sprintf("hostname %s resolves to restricted IP %s: %s", hostname, resolvedIP.String(), reason) } } - // If DNS resolution fails, we allow the URL (the actual request will fail anyway) - // This prevents blocking legitimate URLs when DNS is temporarily unavailable // Check for suspicious port numbers port := parsed.Port()