@@ -385,10 +385,6 @@ func TestReadConfigurationDefinitions_DirectoryTraversal(t *testing.T) {
385385 name : "parent directory traversal with ../" ,
386386 schemaPath : "../../../etc/passwd" ,
387387 },
388- {
389- name : "relative parent traversal" ,
390- schemaPath : "schemas/../../secrets.json" ,
391- },
392388 {
393389 name : "multiple parent traversals" ,
394390 schemaPath : "./../.././../../../sensitive.json" ,
@@ -428,7 +424,7 @@ func TestReadConfigurationDefinitions_DirectoryTraversal(t *testing.T) {
428424
429425 require .NoError (t , err )
430426 assert .NotNil (t , configs )
431- assert .Contains (t , outputStr , "directory traversal " )
427+ assert .Contains (t , outputStr , "must be within workspace " )
432428 })
433429 }
434430}
@@ -530,11 +526,11 @@ func TestReadAgentControlDefinitions_ContentLoadingWarnings(t *testing.T) {
530526 expectedWarning : "failed to load content" ,
531527 },
532528 {
533- name : "directory traversal in content path" ,
529+ name : "content path escapes workspace " ,
534530 setupFunc : func (t * testing.T , tmpDir string ) string {
535531 return "../../../etc/passwd"
536532 },
537- expectedWarning : "directory traversal " ,
533+ expectedWarning : "must be within workspace " ,
538534 },
539535 }
540536
@@ -820,43 +816,15 @@ func TestLoadAndEncodeFile_PathValidation(t *testing.T) {
820816 expectedErrMsg string
821817 }{
822818 {
823- name : "path with directory traversal using ../ " ,
819+ name : "path escapes workspace via parent traversal " ,
824820 setupFunc : func (t * testing.T ) (string , string ) {
825821 tmpDir := t .TempDir ()
826822 workspace := filepath .Join (tmpDir , "workspace" , config .GetRootFolderForAgentRepo ())
827823 require .NoError (t , os .MkdirAll (workspace , 0755 ))
828824
829- // Path that would escape using ../
830825 return filepath .Join (tmpDir , "workspace" ), "../../../etc/passwd"
831826 },
832- expectedErrMsg : "directory traversal" ,
833- },
834- {
835- name : "path with multiple directory traversals" ,
836- setupFunc : func (t * testing.T ) (string , string ) {
837- tmpDir := t .TempDir ()
838- workspace := filepath .Join (tmpDir , "workspace" , config .GetRootFolderForAgentRepo ())
839- require .NoError (t , os .MkdirAll (workspace , 0755 ))
840-
841- return filepath .Join (tmpDir , "workspace" ), "schemas/../../sensitive.json"
842- },
843- expectedErrMsg : "directory traversal" ,
844- },
845- {
846- name : "path attempting to read outside .fleetControl via symlink-like path" ,
847- setupFunc : func (t * testing.T ) (string , string ) {
848- tmpDir := t .TempDir ()
849- workspace := filepath .Join (tmpDir , "workspace" , config .GetRootFolderForAgentRepo ())
850- require .NoError (t , os .MkdirAll (workspace , 0755 ))
851-
852- // Create a file at workspace level (outside .fleetControl)
853- outsideFile := filepath .Join (tmpDir , "workspace" , "outside.json" )
854- require .NoError (t , os .WriteFile (outsideFile , []byte (`{"test": "data"}` ), 0644 ))
855-
856- // Try to access it from .fleetControl
857- return filepath .Join (tmpDir , "workspace" ), "../outside.json"
858- },
859- expectedErrMsg : "directory traversal" ,
827+ expectedErrMsg : "must be within workspace" ,
860828 },
861829 }
862830
@@ -890,3 +858,147 @@ func TestLoadAndEncodeFile_PathValidation(t *testing.T) {
890858 })
891859 }
892860}
861+
862+ // TestLoadAndEncodeFile_OutsideFleetControl verifies that schema files referenced
863+ // from outside .fleetControl (but still within the workspace) load successfully.
864+ // This is the dotnet agent shape: schema lives at ../src/.../Configuration.xsd.
865+ func TestLoadAndEncodeFile_OutsideFleetControl (t * testing.T ) {
866+ tmpDir := t .TempDir ()
867+ workspace := filepath .Join (tmpDir , "workspace" )
868+ configDir := filepath .Join (workspace , config .GetRootFolderForAgentRepo ())
869+ require .NoError (t , os .MkdirAll (configDir , 0755 ))
870+
871+ // Schema lives outside .fleetControl, in a sibling directory inside the workspace.
872+ srcDir := filepath .Join (workspace , "src" )
873+ require .NoError (t , os .MkdirAll (srcDir , 0755 ))
874+ schemaContent := `<?xml version="1.0"?><xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"/>`
875+ schemaFile := filepath .Join (srcDir , "Configuration.xsd" )
876+ require .NoError (t , os .WriteFile (schemaFile , []byte (schemaContent ), 0644 ))
877+
878+ configFile := filepath .Join (configDir , config .GetConfigurationDefinitionsFilename ())
879+ testYAML := `configurationDefinitions:
880+ - platform: linux
881+ description: Test config
882+ type: test-config
883+ version: 1.0.0
884+ format: xml
885+ schema: ../src/Configuration.xsd`
886+ require .NoError (t , os .WriteFile (configFile , []byte (testYAML ), 0644 ))
887+
888+ configs , err := ReadConfigurationDefinitions (context .Background (), workspace )
889+ require .NoError (t , err )
890+ require .Len (t , configs , 1 )
891+
892+ encoded , ok := configs [0 ]["schema" ].(string )
893+ require .True (t , ok , "schema should be base64-encoded string, got %T" , configs [0 ]["schema" ])
894+ expected := base64 .StdEncoding .EncodeToString ([]byte (schemaContent ))
895+ assert .Equal (t , expected , encoded )
896+ }
897+
898+ // TestReadConfigurationDefinitions_DropsBrokenSchemaField verifies that when a
899+ // schema can't be loaded or has the wrong type, the schema field is removed from
900+ // the entry rather than left in place. This prevents the server from rejecting the
901+ // entire bundled request when one entry has a broken schema reference.
902+ func TestReadConfigurationDefinitions_DropsBrokenSchemaField (t * testing.T ) {
903+ tests := []struct {
904+ name string
905+ yamlContent string
906+ }{
907+ {
908+ name : "schema path points to missing file" ,
909+ yamlContent : `configurationDefinitions:
910+ - platform: linux
911+ description: good entry
912+ type: test-config
913+ version: 1.0.0
914+ format: yaml
915+ schema: ./schemas/does-not-exist.json` ,
916+ },
917+ {
918+ name : "schema field is not a string" ,
919+ yamlContent : `configurationDefinitions:
920+ - platform: linux
921+ description: good entry
922+ type: test-config
923+ version: 1.0.0
924+ format: yaml
925+ schema: 12345` ,
926+ },
927+ {
928+ name : "schema path escapes workspace" ,
929+ yamlContent : `configurationDefinitions:
930+ - platform: linux
931+ description: good entry
932+ type: test-config
933+ version: 1.0.0
934+ format: yaml
935+ schema: ../../../etc/passwd` ,
936+ },
937+ }
938+
939+ for _ , tt := range tests {
940+ t .Run (tt .name , func (t * testing.T ) {
941+ tmpDir := t .TempDir ()
942+ configDir := filepath .Join (tmpDir , config .GetRootFolderForAgentRepo ())
943+ require .NoError (t , os .MkdirAll (configDir , 0755 ))
944+
945+ configFile := filepath .Join (configDir , config .GetConfigurationDefinitionsFilename ())
946+ require .NoError (t , os .WriteFile (configFile , []byte (tt .yamlContent ), 0644 ))
947+
948+ configs , err := ReadConfigurationDefinitions (context .Background (), tmpDir )
949+ require .NoError (t , err )
950+ require .Len (t , configs , 1 )
951+
952+ _ , hasSchema := configs [0 ]["schema" ]
953+ assert .False (t , hasSchema , "schema field should be removed when load fails, but got: %v" , configs [0 ]["schema" ])
954+ // Sibling fields stay so the rest of the entry can still ship.
955+ assert .Equal (t , "good entry" , configs [0 ]["description" ])
956+ })
957+ }
958+ }
959+
960+ // TestReadAgentControlDefinitions_DropsBrokenContentField is the agent-control
961+ // counterpart to TestReadConfigurationDefinitions_DropsBrokenSchemaField.
962+ func TestReadAgentControlDefinitions_DropsBrokenContentField (t * testing.T ) {
963+ tests := []struct {
964+ name string
965+ yamlContent string
966+ }{
967+ {
968+ name : "content path points to missing file" ,
969+ yamlContent : `agentControlDefinitions:
970+ - platform: KUBERNETES
971+ supportFromAgent: 1.0.0
972+ supportFromAgentControl: 1.0.0
973+ content: ./agentControl/does-not-exist.yml` ,
974+ },
975+ {
976+ name : "content field is not a string" ,
977+ yamlContent : `agentControlDefinitions:
978+ - platform: KUBERNETES
979+ supportFromAgent: 1.0.0
980+ supportFromAgentControl: 1.0.0
981+ content: 42` ,
982+ },
983+ }
984+
985+ for _ , tt := range tests {
986+ t .Run (tt .name , func (t * testing.T ) {
987+ tmpDir := t .TempDir ()
988+ configDir := filepath .Join (tmpDir , config .GetRootFolderForAgentRepo ())
989+ require .NoError (t , os .MkdirAll (configDir , 0755 ))
990+
991+ agentControlFile := filepath .Join (configDir , config .GetAgentControlDefinitionsFilename ())
992+ require .NoError (t , os .WriteFile (agentControlFile , []byte (tt .yamlContent ), 0644 ))
993+
994+ defs , err := ReadAgentControlDefinitions (context .Background (), tmpDir )
995+ require .NoError (t , err )
996+ require .Len (t , defs , 1 )
997+
998+ _ , hasContent := defs [0 ]["content" ]
999+ assert .False (t , hasContent , "content field should be removed when load fails, but got: %v" , defs [0 ]["content" ])
1000+ // Sibling fields stay so the rest of the entry can still ship.
1001+ assert .Equal (t , "KUBERNETES" , defs [0 ]["platform" ])
1002+ })
1003+ }
1004+ }
0 commit comments