diff --git a/internal/core/comments/comment_service.go b/internal/core/comments/comment_service.go index 70ff9e8..e14275b 100644 --- a/internal/core/comments/comment_service.go +++ b/internal/core/comments/comment_service.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log" + "net/url" "strings" "time" @@ -475,11 +476,20 @@ func (s *commentService) buildPostView(ctx context.Context, post *posts.Post, vi // Avatar is stored as blob in community's repository // Format: https://{pds}/xrpc/com.atproto.sync.getBlob?did={community_did}&cid={avatar_cid} if community.AvatarCID != "" && community.PDSURL != "" { - avatarURLString := fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob?did=%s&cid=%s", - strings.TrimSuffix(community.PDSURL, "/"), - community.DID, - community.AvatarCID) - avatarURL = &avatarURLString + // Validate HTTPS for security (prevent mixed content warnings, MitM attacks) + if !strings.HasPrefix(community.PDSURL, "https://") { + log.Printf("Warning: Skipping non-HTTPS PDS URL for community %s", community.DID) + } else if !strings.HasPrefix(community.AvatarCID, "baf") { + // Validate CID format (IPFS CIDs start with "baf" for CIDv1 base32) + log.Printf("Warning: Invalid CID format for community %s", community.DID) + } else { + // Use proper URL escaping to prevent injection attacks + avatarURLString := fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob?did=%s&cid=%s", + strings.TrimSuffix(community.PDSURL, "/"), + url.QueryEscape(community.DID), + url.QueryEscape(community.AvatarCID)) + avatarURL = &avatarURLString + } } } else { // Log warning but don't fail the entire request diff --git a/internal/core/users/interfaces.go b/internal/core/users/interfaces.go index a8e9022..0107c9e 100644 --- a/internal/core/users/interfaces.go +++ b/internal/core/users/interfaces.go @@ -8,6 +8,26 @@ type UserRepository interface { GetByDID(ctx context.Context, did string) (*User, error) GetByHandle(ctx context.Context, handle string) (*User, error) UpdateHandle(ctx context.Context, did, newHandle string) (*User, error) + + // GetByDIDs retrieves multiple users by their DIDs in a single batch query. + // Returns a map of DID → User for efficient lookups. + // Missing users are not included in the result map (no error for missing users). + // Returns error only on database failures or validation errors (invalid DIDs, batch too large). + // + // Parameters: + // - ctx: Context for cancellation and timeout + // - dids: Array of DIDs to retrieve (must start with "did:", max 1000 items) + // + // Returns: + // - map[string]*User: Map of DID → User for found users + // - error: Validation or database errors (not errors for missing users) + // + // Example: + // userMap, err := repo.GetByDIDs(ctx, []string{"did:plc:abc", "did:plc:xyz"}) + // if err != nil { return err } + // if user, found := userMap["did:plc:abc"]; found { + // // Use user + // } GetByDIDs(ctx context.Context, dids []string) (map[string]*User, error) } diff --git a/internal/db/postgres/user_repo.go b/internal/db/postgres/user_repo.go index 31702d3..9a0e760 100644 --- a/internal/db/postgres/user_repo.go +++ b/internal/db/postgres/user_repo.go @@ -106,6 +106,8 @@ func (r *postgresUserRepo) UpdateHandle(ctx context.Context, did, newHandle stri return user, nil } +const MaxBatchSize = 1000 + // GetByDIDs retrieves multiple users by their DIDs in a single query // Returns a map of DID -> User for efficient lookups // Missing users are not included in the result map (no error for missing users) @@ -114,6 +116,19 @@ func (r *postgresUserRepo) GetByDIDs(ctx context.Context, dids []string) (map[st return make(map[string]*users.User), nil } + // Validate batch size to prevent excessive memory usage and query timeouts + if len(dids) > MaxBatchSize { + return nil, fmt.Errorf("batch size %d exceeds maximum %d", len(dids), MaxBatchSize) + } + + // Validate DID format to prevent SQL injection and malformed queries + // All atProto DIDs must start with "did:" prefix + for _, did := range dids { + if !strings.HasPrefix(did, "did:") { + return nil, fmt.Errorf("invalid DID format: %s", did) + } + } + // Build parameterized query with IN clause // Use ANY($1) for PostgreSQL array support with pq.Array() for type conversion query := `SELECT did, handle, pds_url, created_at, updated_at FROM users WHERE did = ANY($1)`