From 5f57f48673e2054f404b2c5b497a8eaa3690591d Mon Sep 17 00:00:00 2001 From: Florian Dewald Date: Wed, 5 Nov 2025 01:48:55 +0100 Subject: [PATCH] fix(security): validate attachment filenames (#5218) --- server/router/api/v1/attachment_service.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/server/router/api/v1/attachment_service.go b/server/router/api/v1/attachment_service.go index 64f342904..80c32c76b 100644 --- a/server/router/api/v1/attachment_service.go +++ b/server/router/api/v1/attachment_service.go @@ -64,6 +64,9 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat if request.Attachment.Filename == "" { return nil, status.Errorf(codes.InvalidArgument, "filename is required") } + if !validateFilename(request.Attachment.Filename) { + return nil, status.Errorf(codes.InvalidArgument, "filename contains invalid characters or format") + } if request.Attachment.Type == "" { return nil, status.Errorf(codes.InvalidArgument, "type is required") } @@ -325,6 +328,9 @@ func (s *APIV1Service) UpdateAttachment(ctx context.Context, request *v1pb.Updat } for _, field := range request.UpdateMask.Paths { if field == "filename" { + if !validateFilename(request.Attachment.Filename) { + return nil, status.Errorf(codes.InvalidArgument, "filename contains invalid characters or format") + } update.Filename = &request.Attachment.Filename } } @@ -701,3 +707,18 @@ func setResponseHeaders(ctx context.Context, headers map[string]string) error { } return grpc.SetHeader(ctx, metadata.Pairs(pairs...)) } + +func validateFilename(filename string) bool { + // Reject path traversal attempts and make sure no additional directories are created + if !filepath.IsLocal(filename) || strings.ContainsAny(filename, "/\\") { + return false + } + + // Reject filenames starting or ending with spaces or periods + if strings.HasPrefix(filename, " ") || strings.HasSuffix(filename, " ") || + strings.HasPrefix(filename, ".") || strings.HasSuffix(filename, ".") { + return false + } + + return true +}