@@ -7,6 +7,7 @@ package file
77
88import (
99 "bufio"
10+ "bytes"
1011 "context"
1112 "encoding/json"
1213 "errors"
@@ -862,6 +863,7 @@ func (fms *FileManagerService) downloadExternalFile(ctx context.Context, fileAct
862863
863864 fileName := fileAction .File .GetFileMeta ().GetName ()
864865 fms .externalFileHeaders [fileName ] = headers
866+ expectedType := determineExpectedContentType (fileName )
865867
866868 writeErr := fms .fileOperator .Write (
867869 ctx ,
@@ -874,6 +876,16 @@ func (fms *FileManagerService) downloadExternalFile(ctx context.Context, fileAct
874876 return fmt .Errorf ("failed to write downloaded content to temp file %s: %w" , filePath , writeErr )
875877 }
876878
879+ validationErr := fms .validateFileContent (ctx , contentToWrite , fileName , expectedType )
880+ if validationErr != nil {
881+ slog .ErrorContext (ctx , "Security alert: Content validation failed, rejecting file download" ,
882+ "file" , fileName ,
883+ "path" , filePath ,
884+ "error" , validationErr )
885+
886+ return validationErr
887+ }
888+
877889 return nil
878890}
879891
@@ -1033,3 +1045,80 @@ func isMatchesWildcardDomain(hostname, pattern string) bool {
10331045
10341046 return false
10351047}
1048+
1049+ const (
1050+ expectedTypeBinaryModule = "binary_module"
1051+ expectedTypeCertOrKey = "cert_or_key_file"
1052+ expectedTypeNginxConfig = "nginx_config_or_text"
1053+ expectedTypeUnknownText = "unknown_text_default"
1054+ )
1055+
1056+ //nolint:revive,cyclop // calculated cyclomatic is 13
1057+ func (fms * FileManagerService ) validateFileContent (
1058+ ctx context.Context ,
1059+ content []byte ,
1060+ fileName string ,
1061+ expectedType string ,
1062+ ) error {
1063+ contentType := http .DetectContentType (content )
1064+ slog .DebugContext (ctx , "Detected file content type" , "file" , fileName , "type" ,
1065+ contentType , "expected" , expectedType )
1066+
1067+ if strings .Contains (contentType , "application/x-executable" ) ||
1068+ strings .Contains (contentType , "application/x-mach-binary" ) ||
1069+ strings .Contains (contentType , "application/x-elf" ) {
1070+ if expectedType != expectedTypeBinaryModule {
1071+ return fmt .Errorf ("security violation: file is a binary executable (%s), but expected a safe file type" ,
1072+ contentType )
1073+ }
1074+ }
1075+ if expectedType != expectedTypeBinaryModule && strings .Contains (contentType , "application/octet-stream" ) {
1076+ return fmt .Errorf ("security violation: file is an unrecognized binary (%s), but expected a text file" ,
1077+ contentType )
1078+ }
1079+
1080+ switch expectedType {
1081+ case expectedTypeCertOrKey :
1082+ if ! strings .HasPrefix (contentType , "text/" ) {
1083+ return fmt .Errorf ("expected certificate/key file (text), but detected non-text content (%s)" , contentType )
1084+ }
1085+
1086+ if ! bytes .Contains (content , []byte ("-----BEGIN" )) {
1087+ return errors .New ("expected PEM file, but missing the required '-----BEGIN' header" )
1088+ }
1089+
1090+ case expectedTypeNginxConfig :
1091+ if ! strings .HasPrefix (contentType , "text/" ) {
1092+ return fmt .Errorf ("expected NGINX configuration/text file, but detected non-text content (%s)" ,
1093+ contentType )
1094+ }
1095+
1096+ case expectedTypeBinaryModule :
1097+ if strings .HasPrefix (contentType , "text/" ) || strings .Contains (contentType , "application/octet-stream" ) {
1098+ return fmt .Errorf ("expected binary module, but file is not a recognized executable format (%s)" ,
1099+ contentType )
1100+ }
1101+
1102+ case expectedTypeUnknownText :
1103+ if ! strings .HasPrefix (contentType , "text/" ) {
1104+ return fmt .Errorf ("expected general text file, but detected non-text content (%s)" , contentType )
1105+ }
1106+ }
1107+
1108+ return nil
1109+ }
1110+
1111+ func determineExpectedContentType (fileName string ) string {
1112+ ext := strings .ToLower (filepath .Ext (fileName ))
1113+
1114+ switch ext {
1115+ case ".pem" , ".crt" , ".key" :
1116+ return expectedTypeCertOrKey
1117+ case ".conf" , ".cfg" , ".txt" , ".yaml" , ".yml" :
1118+ return expectedTypeNginxConfig
1119+ case ".so" :
1120+ return expectedTypeBinaryModule
1121+ default :
1122+ return expectedTypeUnknownText
1123+ }
1124+ }
0 commit comments