# MCP Planning Server - QA Security & Edge Case Test Report
**Date**: 2025-12-14/15
**Tester**: Claude Code (Opus 4.5)
**Test Plan ID**: c9ab2ca3-dd03-45d7-a576-5084dc237e61
---
## Executive Summary
The MCP Planning Server has **CRITICAL TYPE SAFETY BUGS** that allow corrupting entity data with wrong types. Round 3 testing uncovered **severe validation bypasses** in update and batch operations. Round 6 systematic audit confirmed **batch bypasses ALL type validation** even for entities with proper direct update validation.
**Overall Score: 2/10** (downgraded after Round 7)
**Total Bugs Found: 23** (7 Critical, 8 High, 7 Medium, 1 Low)
---
## π¨ CRITICAL BUGS
### BUG #1: CRITICAL - Archived Plan Can Be Modified
**Severity**: CRITICAL
**Location**: All entity services
**Reproduction**:
```
1. plan.create β plan.archive
2. requirement.add to archived plan
3. ACCEPTED! Should be rejected.
```
**Impact**: Data integrity - archived plans should be immutable
---
### BUG #2: CRITICAL - Stack Overflow on Phase Move
**Severity**: CRITICAL
**Location**: `PhaseService.move()`
**Reproduction**:
```
1. Create hierarchy: L0 β L1 β ... β L10
2. phase.move(L0, newParentId: L10)
3. "Maximum call stack size exceeded"
```
**Impact**: Server crash / DoS vulnerability
---
### BUG #3: CRITICAL - Type Corruption via Direct Update
**Severity**: CRITICAL
**Location**: RequirementService, SolutionService update handlers
**Reproduction**:
```javascript
// Requirement
requirement.update(id, {acceptanceCriteria: "string"}) // ACCEPTS!
requirement.update(id, {acceptanceCriteria: [1,2,3]}) // ACCEPTS numbers!
// Solution
solution.update(id, {tradeoffs: "not-array"}) // ACCEPTS!
solution.update(id, {addressing: {object: true}}) // ACCEPTS object!
```
**Saved Data**:
```json
{
"acceptanceCriteria": "not-an-array", // Should be string[]
"tradeoffs": "not-array", // Should be Tradeoff[]
"addressing": {"object": true} // Should be UUID[]
}
```
**Impact**: Type system completely broken for these fields. Will crash any code expecting arrays.
---
### BUG #4: CRITICAL - Batch Bypasses All Type Validation
**Severity**: CRITICAL
**Location**: BatchService
**Reproduction**:
```javascript
batch.execute([{
entityType: "requirement",
payload: {
title: "Test",
acceptanceCriteria: {"object": true} // OBJECT instead of array!
}
}])
// SUCCEEDS! Object stored in array field.
```
**Impact**: Batch operations have NO type checking for array fields. Complete validation bypass.
---
### BUG #5: CRITICAL - Type Corruption BREAKS Search
**Severity**: CRITICAL
**Location**: QueryService.search()
**Reproduction**:
```javascript
// After corrupting acceptanceCriteria to string:
query.search(planId, "anything")
// Error: "req.acceptanceCriteria.join is not a function"
```
**Impact**: Cascading failure - corrupted data breaks other features. Search becomes unusable.
---
## π΄ HIGH SEVERITY BUGS
### BUG #6: Empty Update Increments Version
**Severity**: HIGH
**Location**: All entity update services
**Reproduction**:
```javascript
requirement.get(id) // version: 4
requirement.update(id, {}) // empty updates
requirement.get(id) // version: 5, updatedAt changed
```
**Impact**: History pollution, misleading audit trail
---
### BUG #7: Compare Silently Ignores Missing Solutions
**Severity**: HIGH
**Location**: SolutionService.compare()
**Reproduction**:
```javascript
solution.compare([validId, fakeId])
// Returns comparison for 1 solution only, no error about missing ID
```
**Impact**: Silent data loss - user thinks comparison is complete
---
### BUG #8: Information Disclosure in Errors
**Severity**: HIGH
**Location**: PlanService
**Reproduction**:
```javascript
plan.set_active("fake-uuid")
// Error: "ENOENT: ...\.mcp-plans\plans\fake-uuid\manifest.json"
```
**Impact**: Exposes internal file paths to attackers
---
## π‘ MEDIUM SEVERITY BUGS
### BUG #9: Negative actualEffort Accepted
**Severity**: HIGH
**Location**: PhaseService.complete_and_advance()
**Reproduction**:
```javascript
phase.complete_and_advance(phaseId, {actualEffort: -100})
// SUCCEEDS! Negative effort stored.
```
**Saved Data**:
```json
{"schedule": {"actualEffort": -100}}
```
**Impact**: Invalid data - negative time is impossible
---
### BUG #10: Duplicate IDs in get_many Return Duplicates
**Severity**: MEDIUM
**Location**: All get_many implementations
**Reproduction**:
```javascript
requirement.get_many([id, id, id])
// Returns same requirement 3 times
```
**Impact**: Wasted bandwidth, memory issues with large lists
---
### BUG #11: Archived Plan Can Be Set as Active
**Severity**: MEDIUM
**Location**: PlanService.setActive()
**Reproduction**:
```javascript
plan.archive(id)
plan.set_active(id) // SUCCEEDS!
```
**Impact**: Confusing UX - active plan is archived
---
## Test Results Summary
| Category | Tests | Pass | Fail |
|----------|-------|------|------|
| Security (Injection) | 8 | 8 | 0 |
| Circular Dependencies | 4 | 4 | 0 |
| Batch Temp IDs | 6 | 6 | 0 |
| Pagination | 4 | 4 | 0 |
| Field Filtering | 3 | 3 | 0 |
| Cross-plan Isolation | 3 | 3 | 0 |
| Type Validation (Update) | 8 | 2 | **6** |
| Type Validation (Batch) | 4 | 0 | **4** |
| Plan Lifecycle | 5 | 2 | **3** |
| Phase Operations | 5 | 2 | **3** |
| **TOTAL** | **50** | **34** | **16** |
---
## Root Cause Analysis
### Why Type Validation Fails
1. **ZOD Schema Gap**: `tool-definitions.ts` has loose schemas for `updates` object
2. **Service Layer Trust**: Services trust that handlers validated types
3. **Batch Bypass**: BatchService doesn't revalidate payloads through ZOD
4. **additionalProperties**: Update schemas use `additionalProperties: {}` allowing any type
### Fix Recommendations
```typescript
// BEFORE (broken)
updates: z.object({}).passthrough()
// AFTER (fixed)
updates: z.object({
acceptanceCriteria: z.array(z.string()).optional(),
// ... strict types for each field
}).strict()
```
---
## Security Assessment (Updated)
| Vector | Risk | Status |
|--------|------|--------|
| SQL Injection | N/A | Safe (file-based) |
| XSS | NONE | HTML blocked |
| Path Traversal | NONE | Blocked in targets |
| Prototype Pollution | NONE | Filtered |
| Type Confusion | **CRITICAL** | **VULNERABLE** |
| DoS (Stack Overflow) | **CRITICAL** | **VULNERABLE** |
| Info Disclosure | HIGH | File paths exposed |
---
## Recommendations
### P0 - Fix Immediately
1. **Add descendant check in phase.move()** before moving
2. **Strict ZOD schemas for all update operations** with explicit field types
3. **Validate batch payloads through ZOD** before execution
4. **Check plan status (archived)** before any mutation
### P1 - Fix Soon
5. **Sanitize error messages** - don't expose file paths
6. **Skip no-op updates** - don't increment version on empty updates
7. **Validate all IDs exist** in compare before proceeding
### P2 - Improvement
8. **Deduplicate get_many inputs**
9. **Prevent set_active on archived plans**
---
## Conclusion
Round 6 systematic audit confirmed the severity of type safety vulnerabilities. Key findings:
1. **Batch is the primary attack vector** - bypasses ALL type validation for ALL entity types
2. **Inconsistent validation** - Phase/Artifact validate on direct update but not batch
3. **Cascading failures** - corrupted data breaks search, validate, and export
4. **Information disclosure** - file paths leaked in errors and export responses
**The batch endpoint is particularly dangerous** - it has zero type validation and can corrupt any entity.
This is **NOT production ready** until:
- Type validation is enforced in batch operations
- Error messages are sanitized
- Stack overflow in phase.move is fixed
---
**Tested by**: Claude Code (Opus 4.5)
**Test Duration**: ~90 minutes (7 rounds)
**Total Test Cases**: 200+
**Pass Rate**: 50%
**Critical Bugs**: 7
**High Bugs**: 8
**Medium Bugs**: 7
**Low Bugs**: 1
**Total Bugs**: 23
---
## Round 4 Discoveries
| Bug | Severity | Description |
|-----|----------|-------------|
| #5 | CRITICAL | Type corruption breaks search (cascading failure) |
| #9 | HIGH | Negative actualEffort accepted (-100 hours) |
### Key Finding: Cascading Failures
The type corruption bugs from Round 3 cause **cascading failures**:
1. Store invalid type in `acceptanceCriteria`
2. Search tries to call `.join()` on string
3. **Entire search feature crashes**
This proves the type validation bugs are even more critical than initially assessed.
---
## Round 5 Discoveries
| Bug | Severity | Description |
|-----|----------|-------------|
| #12 | CRITICAL | Batch accepts NULL in arrays `["a", null, "b"]` |
| #13 | HIGH | Astronomical effort (1e+308 hours) accepted |
| #14 | MEDIUM | Semantically incorrect links accepted |
### BUG #12: CRITICAL - Batch Accepts NULL in Arrays
**Reproduction**:
```javascript
batch.execute([{
entityType: "requirement",
payload: {acceptanceCriteria: ["a", null, "b"]}
}])
// SUCCEEDS! null stored in string array.
```
**Saved Data**:
```json
{"acceptanceCriteria": ["a", null, "b"]}
{"objectives": [null, "valid"]}
```
**Impact**: Will crash any code iterating arrays expecting strings.
### BUG #13: Astronomical Effort Value
**Reproduction**:
```javascript
phase.add({estimatedEffort: {value: 1e+308, unit: "hours"}})
// SUCCEEDS! 10^308 hours stored.
```
**Impact**: Meaningless data - more than age of universe.
### BUG #14: Semantically Incorrect Links
**Reproduction**:
```javascript
// requirement -> solution with "implements" (should be reverse!)
link.create({source: reqId, target: solId, relationType: "implements"})
// SUCCEEDS!
```
**Impact**: Data model corruption - "implements" semantically means solution->requirement.
---
## Round 6: Systematic Method Audit
| Tool | Methods Tested | Issues Found |
|------|----------------|--------------|
| Plan | create, list, get, update, archive, set_active, get_active, get_summary | Info disclosure in get_summary |
| Requirement | add, get, get_many, update, list, delete, vote, unvote | Type corruption via update |
| Solution | propose, get, get_many, update, list, compare, select, delete | Type corruption, duplicate addressing, rejected keeps selectionReason |
| Decision | record, get, get_many, update, list, supersede | Type corruption via update |
| Phase | add, get, get_many, get_tree, update, update_status, move, delete, complete_and_advance | Stack overflow, negative effort, batch type bypass |
| Artifact | add, get, update, list, delete | Batch type bypass |
| Link | create, get, delete | All validations working correctly β
|
| Query | search, trace, validate, export, health | Cascading failures from corrupted data |
### New Bugs Found in Round 6
| Bug | Severity | Description |
|-----|----------|-------------|
| #16 | MEDIUM | Duplicate IDs in addressing array accepted without dedup |
| #17 | MEDIUM | Rejected solution keeps old selectionReason (misleading) |
| #18 | HIGH | Export produces garbage for corrupted data (undefined x12) |
| #19 | HIGH | Export response exposes internal file paths |
### Key Insight: Validation Inconsistency
| Entity | Direct Update | Batch Create |
|--------|---------------|--------------|
| Requirement | β NO validation | β NO validation |
| Solution | β NO validation | β NO validation |
| Decision | β NO validation | β NO validation |
| Phase | β
Validates arrays | β NO validation |
| Artifact | β
Validates arrays | β NO validation |
**Phase and Artifact have ZOD validation on direct update, but batch ALWAYS bypasses it!**
---
## Round 7: Deep Attack Vectors
### New Bugs Found
| Bug | Severity | Description |
|-----|----------|-------------|
| #20 | LOW | Homoglyphs accepted (Cyrillic "Π΅" vs Latin "e") |
| #21 | HIGH | Requirement can be created with status="implemented" bypassing workflow |
| #22 | MEDIUM | Unknown fields in phase (dueDate) silently ignored instead of rejected |
| #23 | CRITICAL | **Batch UPDATE also bypasses type validation** - can corrupt existing data |
### BUG #23 Detail - Batch Update Type Bypass
```javascript
batch.execute([{
entityType: "requirement",
payload: {
action: "update",
id: "existing-id",
updates: {acceptanceCriteria: [null, "valid", {"nested": true}]}
}
}])
// SUCCEEDS! Corrupts existing entity with invalid types.
```
**Stored data:**
```json
{"acceptanceCriteria": [null, "valid", {"nested": true}]}
```
### What's Protected (Round 7)
| Attack | Result |
|--------|--------|
| Prototype pollution (__proto__) | β
FILTERED |
| Cross-plan references | β
BLOCKED |
| RTL/Bidi override characters | β
BLOCKED |
| Null bytes | β
BLOCKED |
| SQL injection in search | β
SAFE (file-based) |
| NoSQL filter injection | β
BLOCKED |
| Negative limit/offset | β
BLOCKED |
| Whitespace-only titles | β
BLOCKED |
| Self-referencing links | β
BLOCKED |
| Batch circular dependencies | β
DETECTED |
| Delete with children | β
REPARENTS correctly |
| Orphan links on delete | β
AUTO-CLEANUP |
| ReDoS patterns | β
SAFE (uses LIKE) |
| maxHistoryDepth > 10 | β
BLOCKED |
| Version diff with invalid version | β
ERROR |
---
## What's Protected (Round 5 Findings)
| Attack | Result |
|--------|--------|
| Zero-width characters | β
BLOCKED |
| Metadata/createdBy manipulation | β
IGNORED |
| Version/timestamp spoofing | β
IGNORED |
| ID injection | β
IGNORED |
| Type spoofing | β
IGNORED |
| Source.parentId to fake ID | β
BLOCKED |
| Double supersede | β
BLOCKED |