Skip to content

Commit 9389d1e

Browse files
MacAttakclaude
andcommitted
test(security): adversarial tests for property/flag name validation and depth limit
Add 30 dedicated tests for security-critical validation boundaries: - 9 invalid property name injection tests (BLOCK-1 regression guard) - 7 valid property name acceptance tests - 1 nested invalid property name test - 5 invalid flag name format tests (WARN-1 regression guard) - 1 invalid arg name format test - 5 valid flag name acceptance tests - 1 schema depth limit test (WARN-3 regression guard) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0fab78a commit 9389d1e

File tree

1 file changed

+217
-0
lines changed

1 file changed

+217
-0
lines changed

internal/manifest/validate_test.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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\nbar"},
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

Comments
 (0)