@@ -2370,3 +2370,220 @@ func TestValidateFlag_ItemSchema_BothInvalidAndNotAllowed_OnlyNotAllowed(t *test
23702370 assert .Nil (t , ve2 ,
23712371 "When item-schema-not-allowed fires, invalid-item-schema should not also fire" )
23722372}
2373+
2374+ // ---------------------------------------------------------------------------
2375+ // Security boundary tests: property names, flag/arg names, depth limit
2376+ // ---------------------------------------------------------------------------
2377+
2378+ func TestValidateFlag_ItemSchema_InvalidPropertyName_Rejected (t * testing.T ) {
2379+ tests := []struct {
2380+ name string
2381+ propName string
2382+ }{
2383+ {"injection via closing brace" , `}); process.exit(1); //` },
2384+ {"injection via quote" , `": z.string(), }); import("child_process"); const x = { "` },
2385+ {"starts with digit" , "123bad" },
2386+ {"contains space" , "foo bar" },
2387+ {"contains hyphen" , "foo-bar" },
2388+ {"contains dot" , "foo.bar" },
2389+ {"empty string" , "" },
2390+ {"contains semicolon" , "foo;bar" },
2391+ {"contains newline" , "foo\n bar" },
2392+ }
2393+ for _ , tt := range tests {
2394+ t .Run (tt .name , func (t * testing.T ) {
2395+ tk := validToolkit ()
2396+ tk .Tools = []Tool {{
2397+ Name : "my-tool" ,
2398+ Description : "A tool" ,
2399+ Entrypoint : "./tool.sh" ,
2400+ Flags : []Flag {{
2401+ Name : "data" ,
2402+ Type : "object" ,
2403+ Description : "Object flag" ,
2404+ ItemSchema : map [string ]any {
2405+ "type" : "object" ,
2406+ "properties" : map [string ]any {
2407+ tt .propName : map [string ]any {"type" : "string" },
2408+ },
2409+ },
2410+ }},
2411+ }}
2412+ errs := Validate (tk )
2413+ ve := findErrorByRule (errs , "invalid-property-name" )
2414+ require .NotNil (t , ve ,
2415+ "Property name %q should be rejected as invalid-property-name, got rules: %v" ,
2416+ tt .propName , errRules (errs ))
2417+ assert .Equal (t , SeverityError , ve .Severity )
2418+ })
2419+ }
2420+ }
2421+
2422+ func TestValidateFlag_ItemSchema_ValidPropertyName_Accepted (t * testing.T ) {
2423+ validNames := []string {"name" , "firstName" , "_private" , "$ref" , "x1" , "A" , "camelCase" }
2424+ for _ , name := range validNames {
2425+ t .Run (name , func (t * testing.T ) {
2426+ tk := validToolkit ()
2427+ tk .Tools = []Tool {{
2428+ Name : "my-tool" ,
2429+ Description : "A tool" ,
2430+ Entrypoint : "./tool.sh" ,
2431+ Flags : []Flag {{
2432+ Name : "data" ,
2433+ Type : "object" ,
2434+ Description : "Object flag" ,
2435+ ItemSchema : map [string ]any {
2436+ "type" : "object" ,
2437+ "properties" : map [string ]any {
2438+ name : map [string ]any {"type" : "string" },
2439+ },
2440+ },
2441+ }},
2442+ }}
2443+ errs := Validate (tk )
2444+ ve := findErrorByRule (errs , "invalid-property-name" )
2445+ assert .Nil (t , ve ,
2446+ "Property name %q should be accepted, but got invalid-property-name" , name )
2447+ })
2448+ }
2449+ }
2450+
2451+ func TestValidateFlag_ItemSchema_NestedInvalidPropertyName_Rejected (t * testing.T ) {
2452+ tk := validToolkit ()
2453+ tk .Tools = []Tool {{
2454+ Name : "my-tool" ,
2455+ Description : "A tool" ,
2456+ Entrypoint : "./tool.sh" ,
2457+ Flags : []Flag {{
2458+ Name : "data" ,
2459+ Type : "object" ,
2460+ Description : "Object flag" ,
2461+ ItemSchema : map [string ]any {
2462+ "type" : "object" ,
2463+ "properties" : map [string ]any {
2464+ "safe" : map [string ]any {
2465+ "type" : "object" ,
2466+ "properties" : map [string ]any {
2467+ "bad name" : map [string ]any {"type" : "string" },
2468+ },
2469+ },
2470+ },
2471+ },
2472+ }},
2473+ }}
2474+ errs := Validate (tk )
2475+ ve := findErrorByRule (errs , "invalid-property-name" )
2476+ require .NotNil (t , ve ,
2477+ "Nested invalid property name should be rejected" )
2478+ }
2479+
2480+ func TestValidateTool_FlagName_InvalidFormat_Rejected (t * testing.T ) {
2481+ tests := []struct {
2482+ name string
2483+ flagName string
2484+ }{
2485+ {"starts with digit" , "1bad" },
2486+ {"contains space" , "foo bar" },
2487+ {"injection via quote" , `foo"; os.Exit(1); //` },
2488+ {"empty string" , "" },
2489+ {"starts with hyphen" , "-flag" },
2490+ }
2491+ for _ , tt := range tests {
2492+ t .Run (tt .name , func (t * testing.T ) {
2493+ tk := validToolkit ()
2494+ tk .Tools = []Tool {{
2495+ Name : "my-tool" ,
2496+ Description : "A tool" ,
2497+ Entrypoint : "./tool.sh" ,
2498+ Flags : []Flag {{
2499+ Name : tt .flagName ,
2500+ Type : "string" ,
2501+ Description : "A flag" ,
2502+ }},
2503+ }}
2504+ errs := Validate (tk )
2505+ ve := findError (errs , "tools[0].flags[0].name" , "name-format" )
2506+ require .NotNil (t , ve ,
2507+ "Flag name %q should be rejected with name-format, got rules: %v" ,
2508+ tt .flagName , errRules (errs ))
2509+ assert .Equal (t , SeverityError , ve .Severity )
2510+ })
2511+ }
2512+ }
2513+
2514+ func TestValidateTool_ArgName_InvalidFormat_Rejected (t * testing.T ) {
2515+ tk := validToolkit ()
2516+ tk .Tools = []Tool {{
2517+ Name : "my-tool" ,
2518+ Description : "A tool" ,
2519+ Entrypoint : "./tool.sh" ,
2520+ Args : []Arg {{
2521+ Name : "123bad" ,
2522+ Type : "string" ,
2523+ Description : "An arg" ,
2524+ }},
2525+ }}
2526+ errs := Validate (tk )
2527+ ve := findError (errs , "tools[0].args[0].name" , "name-format" )
2528+ require .NotNil (t , ve ,
2529+ "Arg name %q should be rejected with name-format" , "123bad" )
2530+ }
2531+
2532+ func TestValidateTool_FlagName_ValidFormat_Accepted (t * testing.T ) {
2533+ validNames := []string {"verbose" , "dry-run" , "output_format" , "V" , "x1" }
2534+ for _ , name := range validNames {
2535+ t .Run (name , func (t * testing.T ) {
2536+ tk := validToolkit ()
2537+ tk .Tools = []Tool {{
2538+ Name : "my-tool" ,
2539+ Description : "A tool" ,
2540+ Entrypoint : "./tool.sh" ,
2541+ Flags : []Flag {{
2542+ Name : name ,
2543+ Type : "string" ,
2544+ Description : "A flag" ,
2545+ }},
2546+ }}
2547+ errs := Validate (tk )
2548+ ve := findError (errs , "tools[0].flags[0].name" , "name-format" )
2549+ assert .Nil (t , ve ,
2550+ "Flag name %q should be accepted, but got name-format error" , name )
2551+ })
2552+ }
2553+ }
2554+
2555+ func TestValidateFlag_ItemSchema_ExceedsMaxDepth_Rejected (t * testing.T ) {
2556+ // Build a schema nested beyond maxItemSchemaDepth.
2557+ schema := map [string ]any {"type" : "string" }
2558+ for i := 0 ; i < maxItemSchemaDepth + 5 ; i ++ {
2559+ schema = map [string ]any {
2560+ "type" : "object" ,
2561+ "properties" : map [string ]any {
2562+ "nested" : schema ,
2563+ },
2564+ }
2565+ }
2566+ tk := validToolkit ()
2567+ tk .Tools = []Tool {{
2568+ Name : "my-tool" ,
2569+ Description : "A tool" ,
2570+ Entrypoint : "./tool.sh" ,
2571+ Flags : []Flag {{
2572+ Name : "data" ,
2573+ Type : "object" ,
2574+ Description : "Deep object" ,
2575+ ItemSchema : schema ,
2576+ }},
2577+ }}
2578+ errs := Validate (tk )
2579+ // Should hit either invalid-property-name (depth limit) or invalid-item-schema (depth limit).
2580+ hasDepthError := false
2581+ for _ , e := range errs {
2582+ if strings .Contains (e .Message , "maximum nesting depth" ) {
2583+ hasDepthError = true
2584+ break
2585+ }
2586+ }
2587+ assert .True (t , hasDepthError ,
2588+ "Schema exceeding maxItemSchemaDepth should produce a depth error, got: %v" , errRules (errs ))
2589+ }
0 commit comments